Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions docs/roleplay.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,47 @@ without a roleplay model) use the same chain. Turns that set `routing.mode` or
`routing.model` keep the adaptive pool, and so does any turn whose chain has no
configured key. Leave the variable unset for the adaptive behaviour above.

### Handing pivotal turns to a stronger model

`ROLEPLAY_HANDOFF_FROM` names chain entries that may pass a turn on, and
`ROLEPLAY_HANDOFF_TO` the entries that take it; both use the
`ROLEPLAY_INTELLIGENCE_MODELS` format and are unset by default. Production lets
DeepSeek V4.1 Flash on OpenCode Go hand off to GLM-5.3 on OpenCode Go, then on
ClinePass. A source request carries one internal function, `multillm_hand_off_turn`,
whose arguments are a reason (`deeper_reasoning` or `stronger_prose`), short notes
and an optional draft. When the source calls it instead of writing, the Worker
discards that attempt (its frames are held until visible text appears, so nothing
reaches the client) and the first available target writes the turn from the same
conversation plus a private note with the source's notes and draft, at its usual
reasoning effort (at `max`, GLM-5.3 spent a 4,096-token budget reasoning and wrote
nothing). If no target accepts the request or writes visible text, the draft is the
reply; without a draft the rest of the chain follows.
A source that writes the reply and also calls the function keeps its reply, and the
call is removed from the stream.

`ROLEPLAY_HANDOFF_MIN_TURNS` (default `2`) allows at most one handoff per that many
turns in a session. Between handoffs, and whenever no target is available, the
source still receives the function with `tool_choice: "none"`, so its prompt prefix
and the provider's cache of it stay the same. Callers that ask for `xhigh` or `max`
reasoning start on the targets. Turns with `routing.fallback: "none"` or a session
tier skip the handoff.

In a test on 2026-10-25 with the function offered, DeepSeek wrote all 12 ordinary
turns itself (scene continuation, conflict, out-of-character recall and small talk)
and handed off 4 of 6 climax and multi-faction planning turns, each with a full
draft of 950 to 1,600 characters. A handoff costs the source's 7 to 15 seconds
before the target starts. Each handoff logs `roleplay_handoff`, and the completion
log counts them in `handoffCount`.

### Empty replies

A reply that finishes with no visible text, only reasoning or nothing at all, is
treated as a failure: the turn starts over on the next chain candidate, at most
twice, and fails with a retry message if none writes anything. This also applies
when the caller sets `max_tokens`. The image-prompt repair keeps its own retry when
it can run. Set `ROLEPLAY_EMPTY_REPLY_FALLBACK_ENABLED=false` to pass empty replies
through. Each retry logs `roleplay_empty_reply_fallback`.

Roleplay generation uses `ROLEPLAY_DEFAULT_REASONING_EFFORT`; the deployment
configuration sets the general default to `high`. OpenCode `glm-5.3-flash`
defaults to `reasoning_effort: max`. Go enables reasoning through that field;
Expand Down Expand Up @@ -609,6 +650,10 @@ Non-secret tuning variables:
| `ROLEPLAY_UPSTREAM_HEADER_TIMEOUT_MS` | `90000` | Maximum wait for provider response headers before advancing to the next tier |
| `ROLEPLAY_PRE_RESPONSE_FALLBACK_ENABLED` | `true` | Advance after a provider transport rejection or header timeout; client aborts never advance |
| `ROLEPLAY_PROVIDER_ERROR_FALLBACK_ENABLED` | `true` | Advance only when a 500/502/504 JSON response explicitly identifies both its error code and type as `provider_error`; unknown 5xx responses remain fail-closed |
| `ROLEPLAY_HANDOFF_FROM` | unset | Chain entries offered `multillm_hand_off_turn`; production uses `opencode:deepseek-v4.1-flash` |
| `ROLEPLAY_HANDOFF_TO` | unset | Chain entries that take a handed-off turn, in chain order; production uses GLM-5.3 on OpenCode Go and ClinePass |
| `ROLEPLAY_HANDOFF_MIN_TURNS` | `2` | At most one handoff per this many turns in a session; `0` or `1` lifts the cap |
| `ROLEPLAY_EMPTY_REPLY_FALLBACK_ENABLED` | `true` | Start a reply with no visible text over on the next candidate, at most twice |
| `ROLEPLAY_REFUSAL_FALLBACK_ENABLED` | `true` | Privately regenerate an exact GLM-5.3 Flash refusal trigger with the configured Uncensored Flash candidate; rejected output is neither streamed nor stored |
| `ROLEPLAY_DEFAULT_REASONING_EFFORT` | `high` | Deliberate generation default with lower latency and hidden-token use than each provider's maximum; callers can still request `max` per turn |
| `ROLEPLAY_STREAM_HEARTBEAT_MS` | `10000` | SSE keepalive interval during upstream silence |
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"lint": "ruff check . --select E9,F63,F7,F82",
"typecheck": "mypy --ignore-missing-imports --follow-imports=skip --disable-error-code import-untyped --disable-error-code var-annotated --disable-error-code arg-type --disable-error-code assignment --disable-error-code operator app.py routes services providers streaming",
"test": "python -m pytest -q",
"test:worker": "node --import ./tests/helpers/network_guard.mjs --test tests/test_cloudflare_worker.mjs tests/test_child_safety_worker.mjs tests/test_child_safety_dashboard.mjs tests/test_runtime_feature_flags_worker.mjs tests/test_anthropic_worker.mjs tests/test_haiku_intelligence_worker.mjs tests/test_cascades_worker.mjs tests/test_cascades_dashboard.mjs tests/test_image_quality_worker.mjs tests/test_secret_scan_worker.mjs tests/test_secret_firewall_worker.mjs tests/test_secret_firewall_coverage_worker.mjs tests/test_roleplay_compaction_budget_worker.mjs tests/test_roleplay_compaction_model_worker.mjs tests/test_roleplay_stream_validation_worker.mjs tests/test_aihubmix_worker.mjs tests/test_gpt_image_moderation_worker.mjs tests/test_dashboard_scripts.mjs tests/test_operations_dashboard.mjs tests/test_opencode_reasoning_worker.mjs tests/test_opencode_thinking_worker.mjs tests/test_nanogpt_reasoning_worker.mjs tests/test_nanogpt_speed_routing_worker.mjs tests/test_sse_heartbeat_worker.mjs tests/test_roleplay_worker.mjs tests/test_roleplay_config_worker.mjs tests/test_roleplay_glm_failover_worker.mjs tests/test_roleplay_intelligence_worker.mjs tests/test_roleplay_memory_worker.mjs tests/test_roleplay_compatibility_worker.mjs tests/test_roleplay_history_reconciliation_worker.mjs tests/test_roleplay_output_contract_analysis.mjs tests/test_roleplay_output_contract_cleanup_worker.mjs tests/test_roleplay_output_contract_nonstream_worker.mjs tests/test_roleplay_output_contract_worker.mjs tests/test_roleplay_performance_worker.mjs tests/test_roleplay_quality_routing_worker.mjs tests/test_roleplay_reasoning_worker.mjs tests/test_roleplay_session_storage_worker.mjs tests/test_roleplay_transport_failure_worker.mjs tests/test_roleplay_unlimited_worker.mjs tests/test_roleplay_workbench_worker.mjs tests/test_roleplay_candidate_context_worker.mjs tests/test_roleplay_operator_worker.mjs tests/test_workbench_stream.mjs tests/test_intelligence_worker.mjs tests/test_intelligence_d1_worker.mjs tests/test_intelligence_auth_worker.mjs tests/test_intelligence_outbound_worker.mjs tests/test_intelligence_operator.mjs tests/test_control_users_worker.mjs tests/test_control_audit_worker.mjs tests/test_access_sso_worker.mjs tests/test_d1_schema_worker.mjs tests/test_shadow_eval_worker.mjs tests/test_auto_routes_worker.mjs tests/test_ai_outbound_worker.mjs tests/test_knowledge_*.mjs tests/test_control_state_worker.mjs tests/test_route_health_worker.mjs tests/test_gateway_mcp_worker.mjs tests/test_usage_ledger_worker.mjs tests/test_media_outbound_worker.mjs tests/test_media_files_worker.mjs tests/test_media_jobs_worker.mjs tests/test_tool_repair_worker.mjs tests/test_prometheus_worker.mjs tests/test_evaluation_noise_floor_worker.mjs tests/test_prompt_templates_worker.mjs tests/test_config_snapshots_worker.mjs tests/test_managed_idempotency_worker.mjs tests/test_wave3_hub_worker.mjs tests/test_wave3_transport_worker.mjs tests/test_admission_leases_worker.mjs tests/test_edge_request_metrics_worker.mjs tests/test_request_cancellation_worker.mjs tests/test_mcp_contract_drift_worker.mjs tests/test_zero_content_retention_worker.mjs tests/test_prompt_cache_cost_worker.mjs tests/test_config_revision_sync_worker.mjs tests/test_gateway_alerts_worker.mjs tests/test_session_tiers_worker.mjs tests/test_usage_reservations_worker.mjs tests/test_shared_generation_cache_worker.mjs tests/test_deferred_tools_worker.mjs tests/test_generation_deadline_worker.mjs tests/test_wave4_worker.mjs tests/test_responses_state_worker.mjs tests/test_gateway_batches_worker.mjs tests/test_semantic_generation_cache_worker.mjs tests/test_canary_traffic_worker.mjs tests/test_context_pages_worker.mjs tests/test_observability_exports_worker.mjs tests/test_prompt_injection_worker.mjs tests/test_pii_rehydration_worker.mjs tests/test_wave5_worker.mjs tests/test_usage_receipts_worker.mjs tests/test_learned_cooldown_worker.mjs tests/test_stream_cost_breaker_worker.mjs tests/test_realtime_worker.mjs tests/test_latency_slo_worker.mjs tests/test_context_canary_worker.mjs tests/test_enterprise_contracts_worker.mjs tests/test_wave6_worker.mjs tests/test_wave7_worker.mjs tests/test_payment_billing_worker.mjs tests/test_credits_ledger_worker.mjs tests/test_scim_provisioning_worker.mjs tests/test_saml_federation_worker.mjs tests/test_tenant_governance_worker.mjs tests/test_tenant_hierarchy_worker.mjs tests/test_frontend_build.mjs tests/test_deploy_pipeline.mjs tests/test_free_routes_worker.mjs tests/test_client_headers_worker.mjs tests/test_key_controls_expand_worker.mjs tests/test_key_data_guards_worker.mjs tests/test_usage_token_details_worker.mjs tests/test_provider_dispatch_worker.mjs tests/test_dispatch_network_worker.mjs tests/test_key_data_preflight.mjs tests/test_account_contract_worker.mjs tests/test_key_controls_contract_worker.mjs",
"test:worker": "node --import ./tests/helpers/network_guard.mjs --test tests/test_cloudflare_worker.mjs tests/test_child_safety_worker.mjs tests/test_child_safety_dashboard.mjs tests/test_runtime_feature_flags_worker.mjs tests/test_anthropic_worker.mjs tests/test_haiku_intelligence_worker.mjs tests/test_cascades_worker.mjs tests/test_cascades_dashboard.mjs tests/test_image_quality_worker.mjs tests/test_secret_scan_worker.mjs tests/test_secret_firewall_worker.mjs tests/test_secret_firewall_coverage_worker.mjs tests/test_roleplay_compaction_budget_worker.mjs tests/test_roleplay_compaction_model_worker.mjs tests/test_roleplay_stream_validation_worker.mjs tests/test_aihubmix_worker.mjs tests/test_gpt_image_moderation_worker.mjs tests/test_dashboard_scripts.mjs tests/test_operations_dashboard.mjs tests/test_opencode_reasoning_worker.mjs tests/test_opencode_thinking_worker.mjs tests/test_nanogpt_reasoning_worker.mjs tests/test_nanogpt_speed_routing_worker.mjs tests/test_sse_heartbeat_worker.mjs tests/test_roleplay_worker.mjs tests/test_roleplay_config_worker.mjs tests/test_roleplay_glm_failover_worker.mjs tests/test_roleplay_handoff_worker.mjs tests/test_roleplay_intelligence_worker.mjs tests/test_roleplay_memory_worker.mjs tests/test_roleplay_compatibility_worker.mjs tests/test_roleplay_history_reconciliation_worker.mjs tests/test_roleplay_output_contract_analysis.mjs tests/test_roleplay_output_contract_cleanup_worker.mjs tests/test_roleplay_output_contract_nonstream_worker.mjs tests/test_roleplay_output_contract_worker.mjs tests/test_roleplay_performance_worker.mjs tests/test_roleplay_quality_routing_worker.mjs tests/test_roleplay_reasoning_worker.mjs tests/test_roleplay_session_storage_worker.mjs tests/test_roleplay_transport_failure_worker.mjs tests/test_roleplay_unlimited_worker.mjs tests/test_roleplay_workbench_worker.mjs tests/test_roleplay_candidate_context_worker.mjs tests/test_roleplay_operator_worker.mjs tests/test_workbench_stream.mjs tests/test_intelligence_worker.mjs tests/test_intelligence_d1_worker.mjs tests/test_intelligence_auth_worker.mjs tests/test_intelligence_outbound_worker.mjs tests/test_intelligence_operator.mjs tests/test_control_users_worker.mjs tests/test_control_audit_worker.mjs tests/test_access_sso_worker.mjs tests/test_d1_schema_worker.mjs tests/test_shadow_eval_worker.mjs tests/test_auto_routes_worker.mjs tests/test_ai_outbound_worker.mjs tests/test_knowledge_*.mjs tests/test_control_state_worker.mjs tests/test_route_health_worker.mjs tests/test_gateway_mcp_worker.mjs tests/test_usage_ledger_worker.mjs tests/test_media_outbound_worker.mjs tests/test_media_files_worker.mjs tests/test_media_jobs_worker.mjs tests/test_tool_repair_worker.mjs tests/test_prometheus_worker.mjs tests/test_evaluation_noise_floor_worker.mjs tests/test_prompt_templates_worker.mjs tests/test_config_snapshots_worker.mjs tests/test_managed_idempotency_worker.mjs tests/test_wave3_hub_worker.mjs tests/test_wave3_transport_worker.mjs tests/test_admission_leases_worker.mjs tests/test_edge_request_metrics_worker.mjs tests/test_request_cancellation_worker.mjs tests/test_mcp_contract_drift_worker.mjs tests/test_zero_content_retention_worker.mjs tests/test_prompt_cache_cost_worker.mjs tests/test_config_revision_sync_worker.mjs tests/test_gateway_alerts_worker.mjs tests/test_session_tiers_worker.mjs tests/test_usage_reservations_worker.mjs tests/test_shared_generation_cache_worker.mjs tests/test_deferred_tools_worker.mjs tests/test_generation_deadline_worker.mjs tests/test_wave4_worker.mjs tests/test_responses_state_worker.mjs tests/test_gateway_batches_worker.mjs tests/test_semantic_generation_cache_worker.mjs tests/test_canary_traffic_worker.mjs tests/test_context_pages_worker.mjs tests/test_observability_exports_worker.mjs tests/test_prompt_injection_worker.mjs tests/test_pii_rehydration_worker.mjs tests/test_wave5_worker.mjs tests/test_usage_receipts_worker.mjs tests/test_learned_cooldown_worker.mjs tests/test_stream_cost_breaker_worker.mjs tests/test_realtime_worker.mjs tests/test_latency_slo_worker.mjs tests/test_context_canary_worker.mjs tests/test_enterprise_contracts_worker.mjs tests/test_wave6_worker.mjs tests/test_wave7_worker.mjs tests/test_payment_billing_worker.mjs tests/test_credits_ledger_worker.mjs tests/test_scim_provisioning_worker.mjs tests/test_saml_federation_worker.mjs tests/test_tenant_governance_worker.mjs tests/test_tenant_hierarchy_worker.mjs tests/test_frontend_build.mjs tests/test_deploy_pipeline.mjs tests/test_free_routes_worker.mjs tests/test_client_headers_worker.mjs tests/test_key_controls_expand_worker.mjs tests/test_key_data_guards_worker.mjs tests/test_usage_token_details_worker.mjs tests/test_provider_dispatch_worker.mjs tests/test_dispatch_network_worker.mjs tests/test_key_data_preflight.mjs tests/test_account_contract_worker.mjs tests/test_key_controls_contract_worker.mjs",
"db:migrate": "python scripts/validate_sqlite_schema.py --apply",
"db:validate": "python scripts/validate_sqlite_schema.py",
"security:secrets": "python scripts/check_static_secrets.py",
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/secret_firewall_coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@
"worker/roleplay/edge.mjs",
"worker/roleplay/endpoint.mjs",
"worker/roleplay/fallback-memory.mjs",
"worker/roleplay/handoff.mjs",
"worker/roleplay/history-reconciliation.mjs",
"worker/roleplay/memory.mjs",
"worker/roleplay/message-fragments.mjs",
Expand Down
1 change: 1 addition & 0 deletions tests/helpers/load_cloudflare_worker.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ function dataModuleUrl(source) {
source = source.replaceAll('from "./worker/provider-dispatch.mjs"', `from "${new URL("../../worker/provider-dispatch.mjs", import.meta.url)}"`);
source = source.replaceAll('from "./worker/key-controls-capability.mjs"', `from "${new URL("../../worker/key-controls-capability.mjs", import.meta.url)}"`);
source = source.replaceAll('from "../provider-dispatch.mjs"', `from "${new URL("../../worker/provider-dispatch.mjs", import.meta.url)}"`);
source = source.replaceAll('from "./handoff.mjs"', `from "${new URL("../../worker/roleplay/handoff.mjs", import.meta.url)}"`);
return `data:text/javascript;base64,${Buffer.from(source, "utf8").toString("base64")}`;
}

Expand Down
21 changes: 13 additions & 8 deletions tests/outbound_inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -2122,17 +2122,17 @@
"worker/roleplay/completion-result.mjs": {
"kind": "non-provider",
"reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.",
"sha256": "924b5dd596ac8ef89f8bd13f04bc7e1067546ed092e47435e3417b66728f1761"
"sha256": "af8ba094f15608f9cb6a47cc13042bb1a89de0dbeb97255a70ec0ad8628b608b"
},
"worker/roleplay/config.mjs": {
"kind": "non-provider",
"reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.",
"sha256": "ea0007c314192ae4718fee801465e8678cf5ec8edd7ffd84e9a70480cc10d1ee"
"sha256": "df8aca19da17bf36a87b3c2c9501f7c950033490288b79a866d75f1fd323c121"
},
"worker/roleplay/continuation.mjs": {
"kind": "non-provider",
"reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.",
"sha256": "0a3abcb3358db0112e4b6e22cf07695947f8bd2d4cde253de033b7f55c29e5b5"
"sha256": "a17e4e907349783bf740f6a240223c488bdb15504f4c44eb4d876c9aadf12fdd"
},
"worker/roleplay/credential-health.mjs": {
"kind": "guarded",
Expand All @@ -2152,7 +2152,7 @@
"worker/roleplay/endpoint.mjs": {
"kind": "internal",
"reason": "Private session handler; generation and compaction use guarded transport.",
"sha256": "9e460b4d4cd420c7692406a7ba03248b73413c830be54423fdb57fe208a6d3cb"
"sha256": "ec959ea02c60bd85a0457f077ee2ce180adc47a5f27ac0167d053394af5d50d7"
},
"worker/roleplay/fallback-memory.mjs": {
"kind": "non-provider",
Expand Down Expand Up @@ -2192,7 +2192,7 @@
"worker/roleplay/nonstream-repair.mjs": {
"kind": "non-provider",
"reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.",
"sha256": "8d3166d81f7be6d0e6f25760a7c05866c4cd5c23eaad36300b8d7f2e3a03e196"
"sha256": "5fea43fd7411c2a5a78ed905333c41750e85c9a5686c0e07a314460bc6a85cf4"
},
"worker/roleplay/operator-edge.mjs": {
"kind": "internal",
Expand Down Expand Up @@ -2277,7 +2277,7 @@
"worker/roleplay/sse-collector.mjs": {
"kind": "non-provider",
"reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.",
"sha256": "d609605183dd3f8f2b90f2f7402ba2f45afb797f88297d11460de31ed36e9997"
"sha256": "5ea6fe478d2158914122d60fb2c0152764dc17de5bfe9ce13c34392d55d2db1f"
},
"worker/roleplay/state-runtime.mjs": {
"kind": "non-provider",
Expand All @@ -2292,12 +2292,12 @@
"worker/roleplay/streaming.mjs": {
"kind": "non-provider",
"reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.",
"sha256": "cfa1ae35acfbddf5d6f8cd362b13c24247b38df78cbca3fd0bc545924a89b372"
"sha256": "edfd06ed6bb1632833f73119a2c6f9d8e3d814d5793edfc81e02fdc00188734e"
},
"worker/roleplay/transport.mjs": {
"kind": "guarded",
"reason": "Every candidate, compaction and fallback uses providerFetch, including Container egress.",
"sha256": "fb1f50e58dd75196d6920a542df1c262e811a1c4e86d701d21366aa1d1540eb3"
"sha256": "28a8b0f142476e847381ecffbcd356158de4431c29fe9a9680078aa5ad6d4e70"
},
"worker/roleplay/turn-runtime.mjs": {
"kind": "non-provider",
Expand Down Expand Up @@ -2453,5 +2453,10 @@
"kind": "internal",
"reason": "Content-free capability probe of the actual fenced expansion Container before legacy private dispatch.",
"sha256": "1bc8f20ae715d46ceed6a32d90d549688d9479ee87fe25a110235d03c390059e"
},
"worker/roleplay/handoff.mjs": {
"kind": "non-provider",
"reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.",
"sha256": "f681974644eb5340debf6d4c0b072bb8dfd5ffcad18a652611d9bdf888d595c3"
}
}
Loading
Loading