symfonic.agent.engine¶
engine ¶
SymfonicAgent -- the compatibility facade. Closed to new work.
Do not build here. The kernel is the front door: new capability work goes through public composition and the generated scaffold, not through this module. A change that adds behaviour to this file is almost certainly in the wrong place, and this file is 14k lines because that was not always said out loud.
What this module still is, stated precisely, because the difference matters and the repository's own invariants forbid collapsing it: this is not dormant and not retired. It still serves turns -- a turn that leaves the migrated envelope falls back to a body here, counted and attributed -- and the legacy implementations remain in the source on purpose. "Closed to new work" is a routing rule for contributors. It is not a claim that the code is unreachable, and it does not authorize deleting anything: physical removal is a separate, explicitly authorized operation behind the fourteen-condition retirement gate.
Fixes that keep a live route correct are still fixes. What does not belong here is new surface.
Provides run() and stream() entry points that transparently
hydrate memory context before LLM execution and consolidate new
memories afterward.
SymfonicAgent ¶
SymfonicAgent(model_provider: ModelProvider, *, graph_preset: str = 'react_loop', config: FrameworkConfig | None = None, graph_backend: GraphBackend | None = None, vector_backend: VectorBackend | None = None, embedding_provider: EmbeddingProvider | None = None, tools: list[Any] | None = None, sub_agents: list[Any] | None = None, conversation_manager: Any | None = None, orchestrator: MemoryOrchestrator | None = None, sleep_consolidator: Any | None = None, metrics_collector: Any | None = None, legacy_pin: Sequence[str] | None = None)
High-level agent combining AgentRuntime + MemoryOrchestrator.
Minimal setup::
agent = SymfonicAgent(model_provider=my_provider)
response = await agent.run("Hello", scope=scope)
Construct the unified agent.
.. versionchanged:: 7.0
The deprecated enable_hms_prompt keyword argument has
been removed. Use FrameworkConfig(enable_hms_prompt=True)
instead — available since v6.0. Passing the removed kwarg
now raises :class:TypeError at construction time.
.. versionadded:: TA8.54
legacy_pin — the documented legacy pin, as capability names
(["invocation.stream"]). It holds exactly the capabilities it
names on their legacy bodies for the life of this agent and moves
no other; an unrecognised name -- or a bare string where a sequence
of names belongs -- is refused by
:func:~symfonic.agent.cutover.legacy_pin.build_legacy_pin rather
than dropped, and a pin on a release at or past
11.0 is refused by LegacyPinRetiredError carrying the
migration procedure. None means "this build sets no pin",
which is not the same value as [] — an empty pin is refused,
because a pin that moves nothing is the silence the mechanism
exists to remove. See evidence/RET-PREP/legacy-pin-wiring.md.
Source code in src/symfonic/agent/engine.py
2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 2685 2686 2687 2688 2689 2690 2691 2692 2693 2694 2695 2696 2697 2698 2699 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716 2717 2718 2719 2720 2721 2722 2723 2724 2725 2726 2727 2728 2729 2730 2731 2732 2733 2734 2735 2736 2737 2738 2739 2740 2741 2742 2743 2744 2745 2746 2747 2748 2749 2750 2751 2752 2753 2754 2755 2756 2757 2758 2759 2760 2761 2762 2763 2764 2765 2766 2767 2768 2769 2770 2771 2772 2773 2774 2775 2776 2777 2778 2779 2780 2781 2782 2783 2784 2785 2786 2787 2788 2789 2790 2791 2792 2793 2794 2795 2796 2797 2798 2799 2800 2801 2802 2803 2804 2805 2806 2807 2808 2809 2810 2811 2812 2813 2814 2815 2816 2817 2818 2819 2820 2821 2822 2823 2824 2825 2826 2827 2828 2829 2830 2831 2832 2833 2834 2835 2836 2837 2838 2839 2840 2841 2842 2843 2844 2845 2846 2847 2848 2849 2850 2851 2852 2853 2854 2855 2856 2857 2858 2859 2860 2861 2862 2863 2864 2865 2866 2867 2868 2869 2870 2871 2872 2873 2874 2875 2876 2877 2878 2879 2880 2881 2882 2883 2884 2885 2886 2887 2888 2889 2890 2891 2892 2893 2894 2895 2896 2897 2898 2899 2900 2901 2902 2903 2904 2905 2906 2907 2908 2909 2910 2911 2912 2913 2914 2915 2916 2917 2918 2919 2920 2921 2922 2923 2924 2925 2926 2927 2928 2929 2930 2931 2932 2933 2934 2935 2936 2937 2938 2939 2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953 2954 2955 2956 2957 2958 2959 2960 2961 2962 2963 2964 2965 2966 2967 2968 2969 2970 2971 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 2987 2988 2989 2990 2991 2992 2993 2994 2995 2996 2997 2998 2999 3000 3001 3002 3003 3004 3005 3006 3007 3008 3009 3010 3011 3012 3013 3014 3015 3016 3017 3018 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072 3073 3074 3075 3076 3077 3078 3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095 3096 3097 3098 3099 3100 3101 3102 3103 3104 3105 3106 3107 3108 3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 3119 3120 3121 3122 3123 3124 3125 3126 3127 3128 3129 3130 3131 3132 3133 3134 3135 3136 3137 3138 3139 3140 3141 3142 3143 3144 3145 3146 3147 3148 3149 3150 3151 3152 3153 3154 3155 3156 3157 3158 3159 3160 3161 3162 3163 3164 3165 3166 3167 3168 3169 3170 3171 3172 3173 3174 3175 3176 3177 3178 3179 3180 3181 3182 3183 3184 3185 3186 3187 3188 3189 3190 3191 3192 3193 3194 3195 3196 3197 3198 3199 3200 3201 3202 3203 3204 3205 3206 3207 3208 3209 3210 3211 3212 3213 3214 3215 3216 3217 3218 3219 3220 3221 3222 3223 3224 3225 3226 3227 3228 3229 3230 3231 3232 3233 3234 3235 3236 3237 3238 3239 3240 3241 3242 3243 3244 3245 3246 3247 3248 3249 3250 3251 3252 3253 3254 3255 3256 3257 3258 3259 3260 3261 3262 3263 3264 3265 3266 3267 3268 3269 3270 3271 3272 3273 3274 3275 3276 3277 3278 3279 3280 3281 3282 3283 3284 3285 3286 3287 3288 3289 3290 3291 3292 3293 3294 3295 3296 3297 3298 3299 3300 3301 3302 3303 3304 3305 3306 3307 3308 3309 3310 3311 3312 3313 3314 3315 3316 3317 3318 3319 3320 3321 3322 3323 3324 3325 3326 3327 3328 3329 3330 3331 3332 3333 3334 3335 3336 3337 3338 3339 3340 3341 3342 3343 3344 3345 3346 3347 3348 3349 3350 3351 3352 3353 3354 3355 3356 3357 3358 3359 3360 3361 3362 3363 3364 3365 3366 3367 3368 3369 3370 3371 3372 3373 3374 3375 3376 3377 3378 3379 3380 3381 3382 3383 3384 3385 3386 3387 3388 3389 3390 3391 3392 3393 3394 3395 3396 3397 3398 3399 3400 3401 3402 3403 3404 3405 3406 3407 3408 3409 3410 3411 3412 3413 3414 3415 3416 3417 3418 3419 3420 3421 3422 3423 3424 3425 3426 3427 3428 3429 3430 3431 3432 3433 3434 3435 3436 3437 3438 3439 3440 3441 3442 3443 3444 3445 3446 3447 3448 3449 3450 3451 3452 3453 3454 3455 3456 3457 3458 3459 3460 3461 3462 3463 3464 3465 3466 3467 3468 3469 3470 3471 3472 3473 3474 3475 3476 3477 3478 3479 3480 3481 3482 3483 3484 3485 3486 3487 3488 3489 3490 3491 3492 3493 3494 3495 3496 3497 3498 3499 3500 3501 3502 3503 3504 3505 3506 3507 3508 3509 3510 3511 3512 3513 3514 3515 3516 3517 3518 3519 3520 3521 3522 3523 3524 3525 3526 | |
cutover
property
¶
The per-capability cutover switchboard for this agent (T3.5.1).
Public because the route, its evidence and its fallback counts have to
be readable from an operator's REPL at 03:00 without importing a
private module: agent.cutover.describe() is the whole procedure.
It used to say agent.cutover.rollback("invocation.run", reason=...)
instead, and that call now refuses: TA8.5 retired the lever on the 11.0
line. Refuses rather than disappears, and says so — an operator who
types the old procedure is told which line took it and why, not handed
an AttributeError.
metrics_collector
property
¶
Return the ConversationMetricsCollector, or None if not configured.
scheduler
property
¶
Access the optional in-process scheduler.
Returns the scheduler instance if one was configured, or None.
aclose
async
¶
Release external resources held by the agent.
Closes the checkpointer factory's connection pool (psycopg pool
for the Postgres path, aiosqlite connection for the SQLite path)
and resets the _checkpointer_ready flag so a fresh astart
runs the next time the agent is used.
Idempotent -- calling aclose twice is safe; the second call
is a no-op because _checkpointer_factory is reset to None
after the first run.
v7.1.3 (Item 10.3): closes the previously-unclosed
psycopg_pool.AsyncConnectionPool opened by
PostgresCheckpointerFactory.astart. Engine re-creation in the
same process (test fixtures, hot-reload, multi-tenant routers)
would otherwise leak pools indefinitely. Pair with FastAPI
lifespan or atexit for clean shutdown.
v9.2.x (issue #27 leak fix): also releases the resources of any
child agents this parent BUILT from a SubAgentSpec -- otherwise
each declarative sub-agent's checkpointer pool leaked on parent
shutdown. Child aclose is pure resource-release + idempotent;
each is guarded so one failure can't strand the others or our own
pool. Caller-supplied pre-built SubAgent children are left
untouched (the caller owns their lifecycle).
Source code in src/symfonic/agent/engine.py
describe_memory_blocks
async
¶
Report per-block memory status for a scope.
Everything this used to require of a caller — construct a
DiscoveryService over the private orchestrator, read
domain.required_labels, then cross-reference enabled_layers —
was configuration policy being re-derived in a route. The enabled/
required decisions belong to the agent that owns the configuration.
Source code in src/symfonic/agent/engine.py
describe_resume_dispatch ¶
Answer "which registration does this token belong to?" (T4.1.2).
A host needs three facts before it can validate a resume body: the
interrupt name the token claims, whether the historical ask_user
contract applies, and which schema validates the payload. Before this
method a host got them by decoding the token with the agent's private
config and reading the agent's private registry — two engine internals
and a policy decision sitting in a route (TRN-1/TRN-2).
The preview decode is deliberately not authoritative and deliberately
swallows every failure: :meth:resume and :meth:resume_interrupt
re-decode and raise the typed error, so a malformed or expired token
still fails in the layer that owns token validation and still produces
the status the shipped contract returns. Making the preview
authoritative would move that decision into transport, where none of
the pause-token suites can observe it.
Two token schemes are previewed, not one (TA8.45). A pause minted
on the kernel route is an EnvelopeSigner envelope rendered by the
deployment's encode_token; PauseToken.decode cannot read one, so
before TA8.45 every such token previewed as the default ask_user
and POST /resume/{token} sent a registered interrupt's redemption
to :meth:resume — the wrong door, with the wrong schema validating
the body. That is a defect on the HTTP path only, which is why it is
found by driving the HTTP path rather than by inferring it from the
Python one. The capability transport is asked first because it is the
scheme that can answer with a registration; the HMAC preview is the
fallback, unchanged, for every token minted before this build.
Source code in src/symfonic/agent/engine.py
flush_background_tasks
async
¶
Await all pending background tasks (e.g. consolidation).
Call this before the event loop shuts down to ensure background work completes. Also shuts down the scheduler if one is configured. Safe to call multiple times.
v9.2.1 (review P1a): awaits only THIS agent's tasks, not the process-global set, so exiting one agent's context manager never blocks on another agent's pending consolidation.
Source code in src/symfonic/agent/engine.py
get_chat_model ¶
v7.16.0 public helper: resolve a role to its bound chat model.
Thin wrapper around resolve_model_config(role) +
self._model_provider.get_chat_model(config). Stateless
and safe to call from any thread or process — the method
reads only self._config and dispatches to the provider's
get_chat_model; it does not touch graph state, the
checkpointer, the orchestrator, or any per-turn mutable. This
is the load-bearing property for background-worker adopters
(Celery, RQ, APScheduler) who instantiate one SymfonicAgent
per process and call this from worker tasks.
Closes the v7.16-era adopter workaround
agent._model_provider.get_chat_model(agent._config.agent.model)
which bypassed the role resolver entirely and therefore missed
any role_models[role] overrides the adopter had set. The
new public method threads through resolve_model_config(role)
so role-routing flows into background workers the same way it
flows into the engine's run() entrypoint.
Default role="action" matches the v7.9.0 wired role -- the
tool-using brain LLM archetype most background workers want.
Other roles defined on FrameworkConfig.role_models resolve
through the same resolver fallback (see
resolve_model_config for the semantics: known role with
override -> returns override; unknown / unset role ->
returns self._config.agent.model).
Note: the engine's internal call sites (react node, summary,
metacognition, reflection, STM summary) continue using the
private self._model_provider reference -- they're
intra-class and already participate in role routing via the
engine's own construction wiring. This public method is for
out-of-graph consumers.
Source code in src/symfonic/agent/engine.py
get_transcript
async
¶
get_transcript(scope: Any, session_id: str, *, index: int | None = None, time_range: tuple[datetime, datetime] | None = None, speaker: str = 'all', limit: int | None = None) -> list[TranscriptMessage]
Read the verbatim conversation transcript for a thread (v7.27.0).
Surface 1 of the three-surface model (verbatim replay) -- reads
state['messages'] from the LangGraph checkpointer, NOT the lossy
episodic HMS surface. Requires a wired checkpointer
(transcript_persistence_enabled=True or ask_user_enabled=True)
AND, for durability across restart, a durable saver (Postgres or
dev_sqlite_checkpoint_path); see
docs/concepts/conversation-persistence.md.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scope
|
Any
|
Tenant scope ( |
required |
session_id
|
str
|
Session identifier completing the |
required |
index
|
int | None
|
0-based ordinal over the |
None
|
time_range
|
tuple[datetime, datetime] | None
|
|
None
|
speaker
|
str
|
|
'all'
|
limit
|
int | None
|
Cap on rows returned (applied last, after index/time
filtering). |
None
|
Returns:
| Type | Description |
|---|---|
list[TranscriptMessage]
|
A list of :class: |
list[TranscriptMessage]
|
is wired, the thread is unknown, or the query matched nothing. |
Raises:
| Type | Description |
|---|---|
TranscriptUnsupportedError
|
|
Source code in src/symfonic/agent/engine.py
4591 4592 4593 4594 4595 4596 4597 4598 4599 4600 4601 4602 4603 4604 4605 4606 4607 4608 4609 4610 4611 4612 4613 4614 4615 4616 4617 4618 4619 4620 4621 4622 4623 4624 4625 4626 4627 4628 4629 4630 4631 4632 4633 4634 4635 4636 4637 4638 4639 4640 4641 4642 4643 4644 4645 4646 4647 4648 4649 4650 4651 4652 4653 4654 4655 4656 4657 4658 4659 4660 4661 4662 4663 4664 4665 4666 4667 4668 4669 4670 4671 4672 4673 4674 4675 4676 4677 4678 4679 4680 4681 4682 4683 4684 4685 4686 4687 4688 4689 4690 4691 4692 4693 4694 4695 4696 4697 4698 4699 | |
interrupt ¶
Build the state-update marker that triggers a registered interrupt.
Roadmap Item 9 surface. Returned from a tool/node body so the
graph's :class:InterruptNode picks it up on the next step and
calls LangGraph's interrupt() after the AIMessage is safely
checkpointed (mirrors the v7.1.0 ask_user topology fix).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
A previously-registered interrupt name. |
required |
payload
|
Any
|
A Pydantic model matching the registered
|
required |
tool_call_id
|
str | None
|
Optional tool-call id when the interrupt is
triggered from a tool; the resume path injects a
|
None
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
The state-update dict to be returned by the calling |
dict[str, Any]
|
tool/node, e.g.:: async def my_tool(...) -> dict[str, Any]: return agent.interrupt( "approval_required", ApprovalPayload(amount=100, reason="..."), ) |
Raises:
| Type | Description |
|---|---|
SymfonicAgentError
|
|
Source code in src/symfonic/agent/engine.py
10336 10337 10338 10339 10340 10341 10342 10343 10344 10345 10346 10347 10348 10349 10350 10351 10352 10353 10354 10355 10356 10357 10358 10359 10360 10361 10362 10363 10364 10365 10366 10367 10368 10369 10370 10371 10372 10373 10374 10375 10376 10377 10378 10379 10380 10381 10382 10383 10384 10385 10386 10387 10388 10389 10390 10391 10392 10393 10394 10395 10396 10397 10398 10399 10400 10401 10402 10403 10404 10405 10406 | |
list_sessions ¶
List the tenant's active chat sessions.
The conversation surface a host needs, so that listing chats is a call
rather than a reach into _session_manager (TRN-2). Scope filtering
stays where it already was — in the session registry — because a host
that could pass any tenant id and get rows back would be the isolation
bug this method exists to make impossible to write by accident.
Source code in src/symfonic/agent/engine.py
load_plugin ¶
Load a domain plugin into the agent -- entirely, or not at all.
Registers the plugin's system prompt contribution and validation hook. Multiple plugins may be loaded; their contributions are chained in registration order.
Admission is atomic (TA8.21). Every reason the kernel route could
not honour the plugin's whole surface is established before any
registry is touched -- see :meth:_admission_refusal -- and the write
that follows rolls back if its second half fails. There is no outcome
in which some of this plugin is loaded. The failure modes this replaced
were all one shape: load_plugin accepted the plugin and a later
stage quietly served less than it declared -- a duplicate name's
policies dropped by compose, a plugin loaded after the seam never
harvested, an unbridgeable plugin's fragment discarded.
Domain tools must be provided at construction time via the
tools= parameter. If a plugin returns tools from
get_domain_tools(), this method raises SymfonicAgentError
because the AgentGraph's ToolRegistry is frozen after compile().
A refused plugin keeps its veto. It is not registered -- it
contributes no prompt fragment, no tool, no lifecycle hook and no name
to any composition, and it is absent from every count this agent
reports -- but it is remembered in a quarantine that
:meth:validate_action asks, deny-only. Refusing a plugin may not
delete a guardrail the adopter installed: an adopter who wrote a veto
and received an exception has not thereby consented to running without
it. See cutover/guardrails.py's _quarantine_outcome.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
plugin
|
Any
|
Any object satisfying the BaseDomainPlugin protocol. |
required |
Raises:
| Type | Description |
|---|---|
SymfonicAgentError
|
The plugin provides tools (which cannot be
registered after the graph has been compiled -- pass domain
tools via |
Source code in src/symfonic/agent/engine.py
6628 6629 6630 6631 6632 6633 6634 6635 6636 6637 6638 6639 6640 6641 6642 6643 6644 6645 6646 6647 6648 6649 6650 6651 6652 6653 6654 6655 6656 6657 6658 6659 6660 6661 6662 6663 6664 6665 6666 6667 6668 6669 6670 6671 6672 6673 6674 6675 6676 6677 6678 6679 6680 6681 6682 6683 6684 6685 6686 6687 6688 6689 6690 6691 6692 6693 | |
on_user_correction
async
¶
on_user_correction(*, turn_id: str, original_response: str, correction_text: str, activation_log: dict[str, Any], scope: FrameworkTenantScope, callbacks: Sequence[CallbackHandler] | None = None) -> None
Dispatch a user correction event to all handlers that support it.
Non-breaking: uses hasattr check at the call site so handlers written before v5.5 are silently skipped.
Source code in src/symfonic/agent/engine.py
register_interrupt ¶
register_interrupt(name: str, payload_schema: type[Any], response_schema: type[Any], *, validate_response: Any = None, cross_scope_allowed: bool = False) -> None
Register a named interrupt point in the graph (Roadmap Item 9).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Stable identifier (e.g. |
required |
payload_schema
|
type[Any]
|
Pydantic |
required |
response_schema
|
type[Any]
|
Pydantic |
required |
validate_response
|
Any
|
Optional cross-validator
|
None
|
cross_scope_allowed
|
bool
|
If True, the resume endpoint accepts a token issued under one scope to be redeemed under another (logs the event for audit). Default False. |
False
|
Raises:
| Type | Description |
|---|---|
SymfonicAgentError
|
|
Source code in src/symfonic/agent/engine.py
10279 10280 10281 10282 10283 10284 10285 10286 10287 10288 10289 10290 10291 10292 10293 10294 10295 10296 10297 10298 10299 10300 10301 10302 10303 10304 10305 10306 10307 10308 10309 10310 10311 10312 10313 10314 10315 10316 10317 10318 10319 10320 10321 10322 10323 10324 10325 10326 10327 10328 10329 10330 10331 10332 10333 10334 | |
resolve_model_config ¶
v7.9.0 public helper: resolve the ModelConfig to use
for a given role.
Returns self._config.role_models[role] when set, falls
back to self._config.agent.model otherwise. Pure function
of the config; safe to call on every turn.
v7.9.0 ships "action" as the only wired role (consumed by
the react node's brain LLM at engine.py:1144-1158 via
AgentConfig injection). Other roles defined on
FrameworkConfig.role_models are reserved for v7.9.1+
wiring at the summary / critic / router / reflection /
consolidation sites. Adopters can still set them today --
the helper resolves them correctly -- but the sites that
consume them haven't been re-pointed yet. Calling this
helper for an unwired role returns the override anyway, so
adopter test code can pin the intended assignment ahead of
the v7.9.1 wiring.
T3.1.1: the precedence itself now lives in
:class:~symfonic.services.models.ModelResolutionService, which is
the same chain agent.backend.model and the per-dispatch resolver
read. The behaviour here is unchanged -- role_models[role] when it
holds a ModelConfig, otherwise the snapshot default -- but it is no
longer a third independent copy of that rule.
Source code in src/symfonic/agent/engine.py
resume
async
¶
resume(pause_token: str, response: AskUserResponse, *, scope: FrameworkTenantScope, callbacks: Sequence[CallbackHandler] | None = None, session_id: str | None = None, run_id: str | None = None, call_id: str | None = None, extra_metadata: dict[str, Any] | None = None, **state_overrides: Any) -> AsyncGenerator[StreamEvent, None]
Resume a paused agent execution from an ask_user elicitation.
Dispatched as of TA8.45. This entry point reads
invocation.continuation on the board and, for a deployment whose
transport can turn the consumer's token back into the envelope it was
minted as, continues the paused turn on the kernel through TA8.35's
redemption. Otherwise :meth:_legacy_continuation_impl serves it, and
still serves every redemption if the switch is put back.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pause_token
|
str
|
Opaque token emitted in the original AskUserQuestionEvent. |
required |
response
|
AskUserResponse
|
The user's selections and optional free-text 'other' field. |
required |
scope
|
FrameworkTenantScope
|
Tenant scope — must match the scope that initiated the pause. |
required |
callbacks
|
Sequence[CallbackHandler] | None
|
Retired by TA8.45 on this door. It was a real,
honoured parameter here before — a non-empty sequence was
forwarded into the graph re-entry — and a non-empty sequence
now raises |
None
|
session_id
|
str | None
|
The conversation this redemption belongs to, if the
transport knows it. Stated axes are checked; see
:meth: |
None
|
run_id
|
str | None
|
The turn this redemption belongs to, if the transport knows it. |
None
|
call_id
|
str | None
|
The question of that turn, if the transport knows it. |
None
|
extra_metadata
|
dict[str, Any] | None
|
Never a parameter of this method. Accepted into
the signature only so an adopter who passes one meets the
shared refusal that names the argument and the line rather than
a bare |
None
|
**state_overrides
|
Any
|
Never a parameter of this method either, and kept for the same reason. |
{}
|
Yields:
| Type | Description |
|---|---|
AsyncGenerator[StreamEvent, None]
|
Typed StreamEvent objects as the run continues. |
Raises:
| Type | Description |
|---|---|
SymfonicAgentError
|
If the token is invalid, expired, or already used, or if the response fails cross-validation against the original request stored in the checkpoint. |
Source code in src/symfonic/agent/engine.py
9578 9579 9580 9581 9582 9583 9584 9585 9586 9587 9588 9589 9590 9591 9592 9593 9594 9595 9596 9597 9598 9599 9600 9601 9602 9603 9604 9605 9606 9607 9608 9609 9610 9611 9612 9613 9614 9615 9616 9617 9618 9619 9620 9621 9622 9623 9624 9625 9626 9627 9628 9629 9630 9631 9632 9633 9634 9635 9636 9637 9638 9639 9640 9641 9642 9643 9644 9645 9646 9647 9648 9649 9650 9651 9652 9653 9654 9655 9656 9657 9658 9659 9660 9661 9662 9663 9664 9665 9666 9667 9668 9669 9670 9671 9672 9673 9674 9675 9676 9677 9678 9679 9680 9681 9682 9683 9684 9685 9686 9687 9688 9689 9690 | |
resume_interrupt
async
¶
resume_interrupt(pause_token: str, response: Any, *, scope: FrameworkTenantScope, callbacks: Sequence[CallbackHandler] | None = None, session_id: str | None = None, run_id: str | None = None, call_id: str | None = None, extra_metadata: dict[str, Any] | None = None, **state_overrides: Any) -> AsyncGenerator[StreamEvent, None]
Resume a generic interrupt (Roadmap Item 9).
Sibling of :meth:resume (which is ask_user-specific).
The router decodes the token first, reads the name claim,
and dispatches here when name != 'ask_user'.
response is an already-validated instance of the registered
response_schema (the router validates the request body
before calling this method).
Dispatched as of TA8.45, and on the same switch as :meth:resume.
They are one atomic segment of a turn — the same token contract, the
same rehydration, the same kernel entry — and giving them two switches
would let a deployment flip ask_user redemption and leave a
registered interrupt on the graph runtime, which is two continuation
semantics for one paused run. The cost of sharing is that a driver
exercising one would certify the other by assumption, so the
reachability probe drives both in every configuration it reports.
Source code in src/symfonic/agent/engine.py
9692 9693 9694 9695 9696 9697 9698 9699 9700 9701 9702 9703 9704 9705 9706 9707 9708 9709 9710 9711 9712 9713 9714 9715 9716 9717 9718 9719 9720 9721 9722 9723 9724 9725 9726 9727 9728 9729 9730 9731 9732 9733 9734 9735 9736 9737 9738 9739 9740 9741 9742 9743 9744 9745 9746 9747 9748 9749 9750 9751 9752 9753 9754 9755 9756 9757 9758 9759 9760 9761 9762 9763 9764 9765 9766 9767 9768 9769 9770 9771 | |
run
async
¶
run(query: str, *, attachments: Sequence[ContentPart] | None = None, scope: FrameworkTenantScope | None = None, callbacks: Sequence[CallbackHandler] | None = None, session_id: str | None = None, history: list[BaseMessage] | None = None, is_admin: bool = False, run_id: str | None = None, extra_metadata: dict[str, Any] | None = None, response_model: type[Any] | None = None, agent_depth: int | None = None, **state_overrides: Any) -> AgentResponse
Execute one turn, on whichever implementation the switch names.
The signature, the return type, and every keyword are unchanged: this is a delegate, not a new API. Two things decide where the turn goes.
cutover.route_for("invocation.run")— the capability switch. It is derived from filed cutover criteria (T3.5.2 files the parity and live-cutover criteria this switch is still waiting on), so it starts onlegacyand an operator can put it back there at any time.- The migrated envelope — whether this invocation needs a capability that has not been flipped yet. A refusal names the feature and is counted, because a fallback nobody can see is a cutover that can be 100% fallback and still look complete.
Everything below the dispatch is unchanged and lives in
:meth:_legacy_run_impl, which stays reachable until T4.4.6 retires it
after the W5 pre-retirement gates pass.
callbacks, extra_metadata and any keyword landing in
**state_overrides are retired on the 11.0 line (TA8.18) and
refuse by name before dispatch — see
:func:~symfonic.agent.engine._refuse_retired_arguments. The keywords
stay in the signature so the failure is a named error citing the line
rather than a bare TypeError.
agent_depth is a typed parameter as of TA8.18. It used to ride in
**state_overrides, which is retired on the 11.0 line; it is the one
key travelling through that map with a home on the migrated side, so it
got a signature rather than an error. Delegation keeps calling
run(..., agent_depth=N) with the same text it always used. It is
refused by the migrated envelope at the same depths it was refused at
while it rode in the map, so the promotion did not re-route delegated
child turns onto the kernel as a side effect -- see the agent_depth
paragraph in :func:~symfonic.agent.cutover.envelope.admit_invocation.
See :meth:_legacy_run_impl for the full argument documentation.
Source code in src/symfonic/agent/engine.py
6890 6891 6892 6893 6894 6895 6896 6897 6898 6899 6900 6901 6902 6903 6904 6905 6906 6907 6908 6909 6910 6911 6912 6913 6914 6915 6916 6917 6918 6919 6920 6921 6922 6923 6924 6925 6926 6927 6928 6929 6930 6931 6932 6933 6934 6935 6936 6937 6938 6939 6940 6941 6942 6943 6944 6945 6946 6947 6948 6949 6950 6951 6952 6953 6954 6955 6956 6957 6958 6959 6960 6961 6962 6963 6964 6965 6966 6967 6968 6969 6970 6971 6972 6973 6974 6975 6976 6977 6978 6979 6980 6981 6982 6983 6984 6985 6986 6987 6988 6989 6990 6991 6992 6993 6994 6995 6996 6997 6998 6999 7000 7001 7002 7003 7004 7005 7006 7007 7008 7009 7010 7011 7012 7013 7014 7015 7016 7017 7018 7019 7020 7021 7022 7023 7024 7025 7026 7027 7028 7029 7030 7031 7032 7033 7034 7035 7036 7037 7038 7039 7040 7041 7042 7043 7044 7045 7046 7047 7048 7049 | |
scrub_properties ¶
Apply the agent's configured credential-key scrubber (public since T4.1.2).
Wraps :func:symfonic.agent.hygiene.scrub_credential_keys with the
pattern compiled from self._config.credential_patterns. When
credential_patterns=[] the pattern is None and this returns the
input unchanged (explicit opt-out).
It is public because a host has to be able to scrub the properties it
accepts on a write path, and reaching for _scrub_props to do it
(five call sites in the FastAPI adapter) made a data-visibility rule
depend on a private attribute — which is exactly the kind of rule a
second host forgets. Removing the credential scrub from a write path is
a security regression, so the call has to be available, not hidden.
Source code in src/symfonic/agent/engine.py
stream
async
¶
stream(query: str, *, attachments: Sequence[ContentPart] | None = None, scope: FrameworkTenantScope | None = None, callbacks: Sequence[CallbackHandler] | None = None, session_id: str | None = None, history: list[BaseMessage] | None = None, is_admin: bool = False, run_id: str | None = None, extra_metadata: dict[str, Any] | None = None, response_model: type[Any] | None = None, agent_depth: int | None = None, **state_overrides: Any) -> AsyncGenerator[StreamChunk, None]
Stream agent execution as a sequence of StreamChunks.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str
|
User query text -- the canonical semantic anchor used for HMS hydration, embeddings, and tool routing. |
required |
attachments
|
Sequence[ContentPart] | None
|
Optional non-text content ( |
None
|
scope
|
FrameworkTenantScope | None
|
Tenant scope for multi-tenant isolation. |
None
|
callbacks
|
Sequence[CallbackHandler] | None
|
Retired on the 11.0 line (TA8.18); supplying handlers
raises |
None
|
session_id
|
str | None
|
Optional session identifier for session tracking. |
None
|
history
|
list[BaseMessage] | None
|
Optional list of prior conversation messages to prepend. |
None
|
is_admin
|
bool
|
True to bypass budget checks (v7.0.3). |
False
|
run_id
|
str | None
|
Optional run identifier for correlation (v7.1.0). It is the identity the kernel route's observability suite attributes every lifecycle and telemetry observation to (TA8.41, C1-R); one is generated when it is omitted. |
None
|
response_model
|
type[Any] | None
|
Not served here. Structured output is a
blocking-turn contract on the 11.0 line -- it binds to the
compiled plan and is delivered on
|
None
|
agent_depth
|
int | None
|
Delegation depth to stamp on this run. A typed
parameter as of TA8.18; it used to ride in
|
None
|
Yields: StreamChunk events with appropriate event_type progression: thinking -> acting/text_delta/tool_call/tool_result -> consolidating -> done
Source code in src/symfonic/agent/engine.py
7797 7798 7799 7800 7801 7802 7803 7804 7805 7806 7807 7808 7809 7810 7811 7812 7813 7814 7815 7816 7817 7818 7819 7820 7821 7822 7823 7824 7825 7826 7827 7828 7829 7830 7831 7832 7833 7834 7835 7836 7837 7838 7839 7840 7841 7842 7843 7844 7845 7846 7847 7848 7849 7850 7851 7852 7853 7854 7855 7856 7857 7858 7859 7860 7861 7862 7863 7864 7865 7866 7867 7868 7869 7870 7871 7872 7873 7874 7875 7876 7877 7878 7879 7880 7881 7882 7883 7884 7885 7886 7887 7888 7889 7890 7891 7892 7893 7894 7895 7896 7897 7898 7899 7900 7901 7902 7903 7904 7905 7906 7907 7908 7909 7910 7911 7912 7913 7914 7915 7916 7917 7918 7919 7920 7921 7922 7923 7924 7925 7926 7927 7928 7929 7930 7931 7932 7933 7934 7935 7936 7937 7938 7939 7940 7941 7942 7943 7944 7945 7946 7947 7948 7949 7950 7951 7952 7953 7954 7955 7956 7957 7958 7959 7960 7961 7962 7963 7964 7965 7966 7967 7968 7969 7970 7971 7972 7973 7974 7975 7976 7977 7978 7979 7980 7981 7982 7983 7984 7985 7986 7987 7988 7989 7990 7991 7992 7993 7994 7995 7996 7997 7998 7999 8000 8001 8002 8003 8004 8005 8006 8007 8008 8009 8010 8011 8012 8013 8014 8015 8016 8017 8018 8019 8020 8021 8022 8023 8024 8025 8026 8027 8028 8029 8030 8031 8032 8033 8034 8035 8036 8037 8038 8039 8040 8041 8042 8043 8044 8045 8046 8047 8048 8049 8050 8051 8052 8053 8054 8055 8056 8057 8058 8059 8060 8061 8062 8063 8064 8065 8066 8067 8068 8069 8070 8071 8072 8073 8074 8075 8076 8077 8078 8079 8080 8081 8082 8083 8084 8085 8086 8087 8088 8089 8090 8091 8092 8093 8094 8095 8096 8097 8098 8099 | |
stream_text
async
¶
stream_text(query: str, *, attachments: Sequence[ContentPart] | None = None, scope: FrameworkTenantScope | None = None, callbacks: Sequence[CallbackHandler] | None = None, session_id: str | None = None, history: list[BaseMessage] | None = None) -> AsyncGenerator[str, None]
Stream only clean response text — the simplest streaming API.
Wraps stream_typed() and yields only the str payload from
TextDeltaEvent objects. All other events (lifecycle, tool calls,
activation, etc.) are silently consumed so consolidation still runs.
Extraction blocks are automatically stripped by the underlying filter.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str
|
User query text -- the canonical semantic anchor used for HMS hydration, embeddings, and tool routing. |
required |
attachments
|
Sequence[ContentPart] | None
|
Optional non-text content ( |
None
|
scope
|
FrameworkTenantScope | None
|
Tenant scope for multi-tenant isolation. |
None
|
callbacks
|
Sequence[CallbackHandler] | None
|
Optional per-invocation callback handlers. |
None
|
session_id
|
str | None
|
Optional session identifier for session tracking. |
None
|
history
|
list[BaseMessage] | None
|
Optional list of prior conversation messages to prepend. |
None
|
Yields:
| Type | Description |
|---|---|
AsyncGenerator[str, None]
|
Clean text strings suitable for direct display in a chat UI. |
Source code in src/symfonic/agent/engine.py
stream_typed
async
¶
stream_typed(query: str, *, attachments: Sequence[ContentPart] | None = None, scope: FrameworkTenantScope | None = None, callbacks: Sequence[CallbackHandler] | None = None, session_id: str | None = None, history: list[BaseMessage] | None = None, is_admin: bool = False, run_id: str | None = None, extra_metadata: dict[str, Any] | None = None, response_model: type[Any] | None = None, agent_depth: int | None = None, **state_overrides: Any) -> AsyncGenerator[StreamEvent, None]
Stream agent execution as typed StreamEvent objects.
Identical hydration, scope injection, and HMS prompt composition
as stream(), but yields typed StreamEvent instances
(TextDeltaEvent, ToolCallStartEvent, ThinkingDeltaEvent, etc.)
instead of StreamChunk wrappers. Designed for library
consumers that want structured event access without parsing raw
LangGraph dicts.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str
|
User query text -- the canonical semantic anchor used for HMS hydration, embeddings, and tool routing. |
required |
attachments
|
Sequence[ContentPart] | None
|
Optional non-text content ( |
None
|
scope
|
FrameworkTenantScope | None
|
Tenant scope for multi-tenant isolation. |
None
|
callbacks
|
Sequence[CallbackHandler] | None
|
Optional per-invocation callback handlers. |
None
|
session_id
|
str | None
|
Optional session identifier for session tracking. |
None
|
history
|
list[BaseMessage] | None
|
Optional list of prior conversation messages to prepend. |
None
|
is_admin
|
bool
|
True to bypass budget checks (v7.0.3). |
False
|
run_id
|
str | None
|
Optional run identifier for correlation (v7.1.0). It is the identity the kernel route's observability suite attributes every lifecycle and telemetry observation to (TA8.41, C1-R); one is generated when it is omitted. |
None
|
response_model
|
type[Any] | None
|
Not served here. Structured output is a
blocking-turn contract on the 11.0 line -- it binds to the
compiled plan and is delivered on
|
None
|
agent_depth
|
int | None
|
Delegation depth to stamp on this run. A typed
parameter as of TA8.18; it used to ride in
|
None
|
**state_overrides
|
Any
|
Retired on the 11.0 line (TA8.18). The keyword
is kept in the signature so an adopter who passes one meets a
refusal that names the argument, cites the line and says what
to do instead, rather than a bare |
{}
|
Yields:
| Type | Description |
|---|---|
AsyncGenerator[StreamEvent, None]
|
Typed StreamEvent objects from the EventTranspiler. |
Source code in src/symfonic/agent/engine.py
8433 8434 8435 8436 8437 8438 8439 8440 8441 8442 8443 8444 8445 8446 8447 8448 8449 8450 8451 8452 8453 8454 8455 8456 8457 8458 8459 8460 8461 8462 8463 8464 8465 8466 8467 8468 8469 8470 8471 8472 8473 8474 8475 8476 8477 8478 8479 8480 8481 8482 8483 8484 8485 8486 8487 8488 8489 8490 8491 8492 8493 8494 8495 8496 8497 8498 8499 8500 8501 8502 8503 8504 8505 8506 8507 8508 8509 8510 8511 8512 8513 8514 8515 8516 8517 8518 8519 8520 8521 8522 8523 8524 8525 8526 8527 8528 8529 8530 8531 8532 8533 8534 8535 8536 8537 8538 8539 8540 8541 8542 8543 8544 8545 8546 8547 8548 8549 8550 8551 8552 8553 8554 8555 8556 8557 8558 8559 8560 8561 8562 8563 8564 8565 8566 8567 8568 8569 8570 8571 8572 8573 8574 8575 8576 8577 8578 8579 8580 8581 8582 8583 8584 8585 8586 8587 8588 8589 8590 8591 8592 8593 8594 8595 8596 8597 8598 8599 8600 8601 8602 8603 8604 8605 8606 8607 8608 8609 8610 8611 8612 8613 8614 8615 8616 8617 8618 8619 8620 8621 8622 8623 8624 8625 8626 8627 8628 8629 8630 8631 8632 8633 8634 8635 8636 8637 8638 8639 8640 8641 8642 8643 8644 8645 8646 8647 8648 8649 8650 8651 8652 8653 8654 8655 8656 8657 8658 8659 8660 8661 8662 8663 8664 8665 8666 8667 8668 8669 8670 8671 8672 8673 8674 8675 8676 8677 8678 8679 8680 8681 8682 8683 8684 8685 8686 8687 8688 8689 8690 8691 8692 8693 8694 8695 8696 8697 8698 8699 8700 8701 8702 8703 8704 8705 8706 8707 8708 8709 8710 8711 8712 8713 8714 8715 8716 8717 8718 8719 8720 8721 8722 8723 8724 8725 8726 8727 8728 8729 8730 8731 8732 8733 8734 8735 8736 8737 8738 8739 8740 8741 8742 8743 8744 8745 8746 8747 8748 8749 8750 8751 8752 8753 8754 8755 8756 8757 8758 8759 8760 8761 | |
validate_action
async
¶
Check all loaded plugins' guardrails for an action.
Asks every registered plugin's validate_state_transition hook and
returns False as soon as one rejects. Plugin errors are non-fatal and
default to allowing the action.
The one enforcement point, on both routes (TA8.21). The body used
to be an inline loop over self._plugins, which meant a turn served
by the kernel had no guardrail path at all -- the plugin's policy was a
contribution nothing folded. It now delegates to
:func:symfonic.agent.cutover.guardrails.enforce_guardrails over the
capability the bundle folds, which is the shape TA8.19 gave
session_id: one derivation site, called from the position the legacy
body inlined it, rather than two implementations that agree by
inspection until one of them changes.
The observable contract is unchanged and that is deliberate:
- deny-wins, and the first refusal short-circuits;
- a plugin with no
validate_state_transitioncontributes no policy at all, rather than an always-allow one. An always-allow stage in a trace is indistinguishable from a stage that examined the action.
One deliberate change of verdict, and it is a security fix. A hook that raises abstains, and an abstention now denies (TA8.21). The inline loop this replaced swallowed the exception and allowed, which is SEC-FCP-4 / TM-17, filed HIGH: a guard whose backend was down was indistinguishable from a guard that approved, and one unhandled exception inside a policy was an agent with no guardrails. The abstention remains a value on the outcome -- "the only guard with an opinion was down" and "nobody objected" are still different readings of a turn -- they simply no longer produce the same answer.
Refused plugins are asked too, deny-only. load_plugin admits a
plugin whole or not at all, and a plugin it refused is not registered
anywhere. Its veto is not thereby deleted: the quarantine is passed
alongside, and it can refuse an action but never approve one. Otherwise
a refusal -- a duplicate name, an unbridgeable surface -- would be a way
to remove a guardrail, and "no path turns a veto into an allow" is
the property this method exists to hold.
One narrowing, named rather than left to be found. The inline loop
handed each hook the caller's own context object; a guard now
receives a fresh mutable dict taken from
:class:~symfonic.capabilities.extensions.values.PolicyRequest's frozen
view (capabilities/extensions/bridge.py's legacy_guard_policies).
So a plugin that annotated the context inside its guard -- stamping a
reason the caller read back afterwards -- no longer reaches the caller's
mapping, on either route. That is deliberate and is AS-INT-3's rule:
a policy answers a question, it does not edit the question, and a guard
handed the live tool arguments could otherwise rewrite the very call it
was only allowed to veto. The verdict contract is unchanged; the
side-channel is gone.
The plugin list is passed alongside the capability as the floor, not
as a second route. A capability that could not be built answers None
(see :meth:_extensions_capability), and delegating that to an
unconditional allow would mean an agent with plugins loaded enforcing no
guardrail at all -- strictly more permissive than the loop this
replaced, on both routes at once. enforce_guardrails asks the
population directly in that case, reusing the same deny-wins combinator,
so a failed build costs the fragments it was carrying and not the vetoes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
action
|
str
|
Name of the action/tool being attempted. |
required |
context
|
dict[str, Any]
|
Current execution context (tenant, params, etc.) |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if all plugins allow the action, False if any block it. |
Source code in src/symfonic/agent/engine.py
6711 6712 6713 6714 6715 6716 6717 6718 6719 6720 6721 6722 6723 6724 6725 6726 6727 6728 6729 6730 6731 6732 6733 6734 6735 6736 6737 6738 6739 6740 6741 6742 6743 6744 6745 6746 6747 6748 6749 6750 6751 6752 6753 6754 6755 6756 6757 6758 6759 6760 6761 6762 6763 6764 6765 6766 6767 6768 6769 6770 6771 6772 6773 6774 6775 6776 6777 6778 6779 6780 6781 6782 6783 6784 6785 6786 6787 6788 6789 6790 6791 6792 6793 | |