@vellumai/assistant 0.8.7-dev.202606052232.2ddc989 → 0.8.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bun.lock +2 -2
- package/docs/plugins.md +832 -0
- package/examples/plugins/echo/README.md +60 -61
- package/examples/plugins/echo/package.json +2 -1
- package/examples/plugins/echo/register.ts +143 -0
- package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +6 -7
- package/openapi.yaml +5 -15
- package/package.json +2 -2
- package/src/__tests__/agent-loop-exit-reason.test.ts +56 -3
- package/src/__tests__/anthropic-provider.test.ts +1 -1
- package/src/__tests__/app-control-flow.test.ts +1 -1
- package/src/__tests__/app-dir-path-guard.test.ts +0 -1
- package/src/__tests__/approval-routes-http.test.ts +1 -4
- package/src/__tests__/channel-approval-routes.test.ts +1 -1
- package/src/__tests__/channel-approvals.test.ts +1 -1
- package/src/__tests__/circuit-breaker-pipeline.test.ts +405 -0
- package/src/__tests__/compaction-pipeline.test.ts +210 -0
- package/src/__tests__/compaction-timeout-recovery.test.ts +251 -0
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +3 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +3 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +7 -3
- package/src/__tests__/conversation-agent-loop.test.ts +39 -42
- package/src/__tests__/conversation-clean-command.test.ts +2 -5
- package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -4
- package/src/__tests__/conversation-runtime-assembly.test.ts +71 -140
- package/src/__tests__/conversation-runtime-workspace.test.ts +27 -108
- package/src/__tests__/conversation-starter-routes.test.ts +6 -14
- package/src/__tests__/conversation-workspace-cache-state.test.ts +16 -17
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -61
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -7
- package/src/__tests__/db-acp-history.test.ts +0 -101
- package/src/__tests__/dynamic-page-surface.test.ts +0 -31
- package/src/__tests__/file-write-tool.test.ts +0 -63
- package/src/__tests__/gateway-only-guard.test.ts +2 -12
- package/src/__tests__/guardian-grant-minting.test.ts +1 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +4 -2
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +0 -1
- package/src/__tests__/heartbeat-service.test.ts +0 -1
- package/src/__tests__/host-app-control-routes.test.ts +1 -1
- package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
- package/src/__tests__/injector-background-turn.test.ts +1 -1
- package/src/__tests__/injector-chain.test.ts +6 -34
- package/src/__tests__/injector-disk-pressure.test.ts +34 -77
- package/src/__tests__/injector-document-comments.test.ts +1 -1
- package/src/__tests__/list-messages-hidden-metadata.test.ts +0 -38
- package/src/__tests__/memory-v2-static-injector.test.ts +1 -1
- package/src/__tests__/{overflow-reduction-loop.test.ts → overflow-reduce-pipeline.test.ts} +284 -64
- package/src/__tests__/pipeline-runner.test.ts +554 -0
- package/src/__tests__/plugin-api-shim.test.ts +6 -3
- package/src/__tests__/plugin-bootstrap.test.ts +23 -12
- package/src/__tests__/plugin-registry.test.ts +49 -3
- package/src/__tests__/plugin-types.test.ts +70 -0
- package/src/__tests__/reaction-persistence.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +1 -4
- package/src/__tests__/skill-feature-flags-integration.test.ts +0 -33
- package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
- package/src/__tests__/subagent-fork-notifications.test.ts +3 -1
- package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
- package/src/__tests__/subagent-manager-notify.test.ts +3 -1
- package/src/__tests__/subagent-notify-parent.test.ts +3 -1
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
- package/src/__tests__/user-plugin-loader.test.ts +286 -54
- package/src/acp/__tests__/client-handler.test.ts +0 -40
- package/src/acp/__tests__/prepare-agent-env.test.ts +0 -137
- package/src/acp/__tests__/session-manager-persistence.test.ts +28 -95
- package/src/acp/agent-process.ts +1 -61
- package/src/acp/client-handler.ts +0 -31
- package/src/acp/prepare-agent-env.ts +29 -83
- package/src/acp/resolve-agent.test.ts +7 -320
- package/src/acp/resolve-agent.ts +18 -182
- package/src/acp/session-manager.ts +73 -495
- package/src/acp/types.ts +0 -8
- package/src/agent/compaction-circuit.ts +102 -60
- package/src/agent/loop.ts +59 -32
- package/src/api/responses/conversation-message.ts +1 -7
- package/src/approvals/guardian-request-resolvers.ts +1 -1
- package/src/background-wake/next-wake.ts +0 -1
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
- package/src/config/acp-defaults.test.ts +0 -10
- package/src/config/acp-defaults.ts +0 -6
- package/src/config/bundled-skills/acp/SKILL.md +31 -83
- package/src/config/bundled-skills/acp/TOOLS.json +4 -4
- package/src/config/bundled-skills/app-builder/SKILL.md +381 -224
- package/src/config/bundled-skills/app-builder/TOOLS.json +0 -29
- package/src/config/bundled-skills/document-editor/SKILL.md +23 -28
- package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
- package/src/config/bundled-tool-registry.ts +0 -2
- package/src/config/feature-flag-registry.json +5 -14
- package/src/config/schemas/heartbeat.ts +0 -9
- package/src/context/strip-injections.ts +2 -8
- package/src/context/window-manager.ts +1 -2
- package/src/daemon/conversation-agent-loop-handlers.ts +11 -0
- package/src/daemon/conversation-agent-loop.ts +279 -62
- package/src/daemon/conversation-runtime-assembly.ts +69 -106
- package/src/daemon/conversation-store.ts +90 -9
- package/src/daemon/conversation-workspace.ts +0 -17
- package/src/daemon/conversation.ts +6 -0
- package/src/daemon/external-plugins-bootstrap.ts +11 -11
- package/src/daemon/handlers/conversations.ts +1 -3
- package/src/daemon/handlers/skills.ts +1 -4
- package/src/daemon/lifecycle.ts +0 -21
- package/src/daemon/server.ts +0 -2
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -3
- package/src/heartbeat/heartbeat-run-store.ts +1 -23
- package/src/heartbeat/heartbeat-service.ts +0 -26
- package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
- package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
- package/src/ipc/skill-routes/__tests__/memory.test.ts +0 -15
- package/src/ipc/skill-routes/memory.ts +2 -4
- package/src/memory/conversation-starter-checkpoints.ts +0 -1
- package/src/memory/db-init.ts +0 -2
- package/src/memory/job-handlers/conversation-starters.ts +2 -13
- package/src/memory/jobs-worker.ts +1 -1
- package/src/memory/migrations/index.ts +0 -1
- package/src/memory/schema/acp.ts +0 -4
- package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
- package/src/memory/v2/consolidation-job.ts +4 -13
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/assign.test.ts +4 -4
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/live-integration.test.ts +4 -4
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/maintain-job.test.ts +5 -5
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/orchestrate.test.ts +3 -3
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/reconcile.test.ts +2 -2
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/render-injection.test.ts +1 -1
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/router.test.ts +3 -3
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/selection-log-store.test.ts +8 -8
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/selector.test.ts +3 -3
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/shadow-plugin.test.ts +12 -12
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/assign.ts +5 -5
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/capabilities.ts +2 -2
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/maintain-job.ts +8 -8
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/page-content.ts +2 -2
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/provider-blocks.ts +1 -1
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/reconcile.ts +3 -3
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/render-injection.ts +1 -1
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/router.ts +3 -3
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/selection-log-store.ts +4 -4
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/selector.ts +4 -4
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/shadow-plugin.ts +90 -28
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/tree.ts +1 -1
- package/src/plugin-api/index.ts +5 -0
- package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +93 -0
- package/src/plugins/defaults/{memory-v3-shadow → circuit-breaker}/package.json +2 -2
- package/src/plugins/defaults/circuit-breaker/register.ts +39 -0
- package/src/plugins/defaults/compaction/middlewares/compaction.ts +25 -0
- package/src/plugins/defaults/compaction/package.json +1 -1
- package/src/plugins/defaults/compaction/register.ts +19 -8
- package/src/plugins/defaults/compaction/terminal.ts +73 -0
- package/src/plugins/defaults/index.ts +5 -3
- package/src/plugins/defaults/{memory-retrieval/injectors.ts → injectors/register.ts} +7 -45
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +7 -11
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
- package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +126 -0
- package/src/plugins/defaults/overflow-reduce/package.json +15 -0
- package/src/plugins/defaults/overflow-reduce/register.ts +42 -0
- package/src/plugins/external-api.ts +2 -2
- package/src/plugins/pipeline.ts +293 -6
- package/src/plugins/registry.ts +37 -9
- package/src/plugins/types.ts +336 -32
- package/src/plugins/user-loader.ts +127 -30
- package/src/proactive-artifact/aux-message-injector.ts +1 -1
- package/src/proactive-artifact/job.test.ts +1 -1
- package/src/prompts/__tests__/system-prompt.test.ts +0 -6
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +2 -4
- package/src/runtime/__tests__/agent-wake.test.ts +5 -5
- package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
- package/src/runtime/agent-wake.ts +3 -0
- package/src/runtime/assistant-event-hub.ts +1 -1
- package/src/runtime/channel-approvals.ts +1 -1
- package/src/runtime/interactive-ui.ts +1 -1
- package/src/runtime/routes/__tests__/acp-routes.test.ts +55 -283
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +4 -5
- package/src/runtime/routes/__tests__/surface-content-routes.test.ts +1 -4
- package/src/runtime/routes/acp-routes.test.ts +25 -89
- package/src/runtime/routes/acp-routes.ts +29 -81
- package/src/runtime/routes/approval-routes.ts +1 -1
- package/src/runtime/routes/browser-routes.ts +1 -1
- package/src/runtime/routes/browser-tabs-routes.ts +10 -6
- package/src/runtime/routes/conversation-cli-routes.ts +1 -1
- package/src/runtime/routes/conversation-list-routes.ts +1 -1
- package/src/runtime/routes/conversation-query-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +2 -15
- package/src/runtime/routes/conversation-starter-routes.ts +7 -13
- package/src/runtime/routes/conversations-import-routes.ts +7 -24
- package/src/runtime/routes/host-app-control-routes.ts +1 -1
- package/src/runtime/routes/host-cu-routes.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +3 -18
- package/src/runtime/routes/inbound-message-handler.ts +1 -1
- package/src/runtime/routes/memory-v3-routes.ts +6 -16
- package/src/runtime/routes/playground/helpers.ts +1 -1
- package/src/runtime/routes/surface-conversation-resolver.ts +3 -4
- package/src/runtime/routes/work-items-routes.ts +4 -2
- package/src/runtime/services/conversation-serializer.ts +1 -1
- package/src/signals/cancel.ts +4 -2
- package/src/subagent/manager.ts +5 -17
- package/src/tools/acp/list-agents.test.ts +1 -7
- package/src/tools/acp/spawn.test.ts +55 -158
- package/src/tools/acp/spawn.ts +72 -47
- package/src/tools/acp/steer.test.ts +8 -105
- package/src/tools/acp/steer.ts +17 -48
- package/src/tools/apps/executors.ts +8 -13
- package/src/tools/filesystem/write.ts +0 -34
- package/src/tools/subagent/spawn.ts +4 -2
- package/src/tools/ui-surface/definitions.ts +4 -25
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +5 -4
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +45 -69
- package/examples/plugins/echo/hooks/post-tool-use.ts +0 -18
- package/examples/plugins/echo/hooks/stop.ts +0 -16
- package/examples/plugins/echo/hooks/user-prompt-submit.ts +0 -18
- package/examples/plugins/echo/src/emit.ts +0 -19
- package/src/__tests__/compaction-circuit.test.ts +0 -258
- package/src/__tests__/compaction-direct.test.ts +0 -132
- package/src/__tests__/conversations-import-system-filter.test.ts +0 -101
- package/src/acp/__tests__/agent-process.test.ts +0 -161
- package/src/acp/__tests__/helpers/acp-history-db.ts +0 -82
- package/src/acp/__tests__/helpers/exec-file-stub.ts +0 -101
- package/src/acp/__tests__/session-manager-resume.test.ts +0 -736
- package/src/acp/auto-install.test.ts +0 -196
- package/src/acp/auto-install.ts +0 -177
- package/src/acp/feature-gate.test.ts +0 -48
- package/src/acp/feature-gate.ts +0 -34
- package/src/acp/resume-hint.ts +0 -25
- package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +0 -48
- package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +0 -57
- package/src/config/bundled-skills/app-builder/references/SLIDES.md +0 -38
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -62
- package/src/daemon/conversation-registry.ts +0 -159
- package/src/daemon/overflow-reduction-loop.ts +0 -230
- package/src/memory/migrations/272-acp-session-history-cwd.ts +0 -36
- package/src/plugins/defaults/compaction/compact.ts +0 -59
- package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +0 -14
- package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +0 -19
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +0 -75
- package/src/plugins/defaults/memory-v3-shadow/register.ts +0 -26
- package/src/tools/acp/context.ts +0 -20
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/capabilities.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/core.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/fixtures/eval-turns.json +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/fixtures/live-turns.json +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/health.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/needle.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/provider-blocks.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/snapshot.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/tree.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/types.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/working-set-eviction.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/working-set-skeleton.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/core.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/README.md +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/assignments.json +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/core.json +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/leaves/domain-a/topic-x.md +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/leaves/domain-a/topic-y.md +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/leaves/domain-b/topic-z.md +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/health.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/llm-retry.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/needle.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/orchestrate.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/snapshot.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/types.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/working-set.ts +0 -0
|
@@ -47,8 +47,6 @@ import { resolve } from "node:path";
|
|
|
47
47
|
|
|
48
48
|
import { getConfig } from "../../../config/loader.js";
|
|
49
49
|
import type { InjectionMatcher } from "../../../context/strip-injections.js";
|
|
50
|
-
import { findConversationOrSubagent } from "../../../daemon/conversation-registry.js";
|
|
51
|
-
import { resolveWorkspaceTopLevelContext } from "../../../daemon/conversation-workspace.js";
|
|
52
50
|
import { readNowScratchpad } from "../../../daemon/now-scratchpad.js";
|
|
53
51
|
import { getInContextPkbPaths } from "../../../daemon/pkb-context-tracker.js";
|
|
54
52
|
import { buildPkbReminder } from "../../../daemon/pkb-reminder-builder.js";
|
|
@@ -127,22 +125,12 @@ Then help the user clean up storage. Prefer safe inspection steps first, such as
|
|
|
127
125
|
Do not work on unrelated tasks until enough space is freed to clear the lock or the user explicitly overrides it. Background processes and messages from trusted contacts are blocked while this cleanup mode is active.
|
|
128
126
|
</disk_pressure_warning>`;
|
|
129
127
|
|
|
130
|
-
/**
|
|
131
|
-
* `disk-pressure-warning` injector — order 5, prepend-user-tail.
|
|
132
|
-
*
|
|
133
|
-
* Emits the storage cleanup-mode warning at the very top of the user tail when
|
|
134
|
-
* the turn is restricted to disk-pressure cleanup. Reads the cleanup-mode flag
|
|
135
|
-
* off the live `Conversation` looked up by conversation id — the agent loop
|
|
136
|
-
* sets `diskPressureCleanupModeActive` when it classifies the turn's
|
|
137
|
-
* disk-pressure policy — rather than having the loop thread it as an injection
|
|
138
|
-
* input.
|
|
139
|
-
*/
|
|
140
128
|
const diskPressureWarningInjector: Injector = {
|
|
141
129
|
name: "disk-pressure-warning",
|
|
142
130
|
order: DEFAULT_INJECTOR_ORDER.diskPressureWarning,
|
|
143
131
|
async produce(ctx: TurnContext): Promise<InjectionBlock | null> {
|
|
144
|
-
const
|
|
145
|
-
if (!
|
|
132
|
+
const inputs = readInjectionInputs(ctx);
|
|
133
|
+
if (!inputs.diskPressureContext?.cleanupModeActive) return null;
|
|
146
134
|
return {
|
|
147
135
|
id: "disk-pressure-warning",
|
|
148
136
|
text: DISK_PRESSURE_WARNING_PROMPT,
|
|
@@ -162,52 +150,26 @@ function isPkbInjectionSilencedByV2(): boolean {
|
|
|
162
150
|
return getConfig().memory.v2.enabled;
|
|
163
151
|
}
|
|
164
152
|
|
|
165
|
-
/**
|
|
166
|
-
* Matchers that mark a persisted `<workspace>` top-level injection. Uses the
|
|
167
|
-
* `{ prefix, suffix }` wrapper shape so user-authored text merely starting with
|
|
168
|
-
* `<workspace>\n` is never mistaken for an injection — matching the full-wrapper
|
|
169
|
-
* requirement the compaction strip uses for this block. The legacy
|
|
170
|
-
* `<workspace_top_level>` tag (pre-rename history) counts as present too.
|
|
171
|
-
*/
|
|
172
|
-
const WORKSPACE_BLOCK_MATCHERS: readonly InjectionMatcher[] = [
|
|
173
|
-
{ prefix: "<workspace>\n", suffix: "\n</workspace>" },
|
|
174
|
-
"<workspace_top_level>",
|
|
175
|
-
];
|
|
176
|
-
|
|
177
153
|
/**
|
|
178
154
|
* `workspace-context` injector — order 10, prepend-user-tail.
|
|
179
155
|
*
|
|
180
156
|
* Injects the workspace top-level directory context at the very top of the
|
|
181
|
-
* user tail's content so the assistant sees a workspace grounding block
|
|
182
|
-
* any other per-turn context.
|
|
183
|
-
*
|
|
184
|
-
* Sources the dirty-guarded top-level cache itself via
|
|
185
|
-
* {@link resolveWorkspaceTopLevelContext} (keyed by conversation id) rather than
|
|
186
|
-
* having the agent loop compute and thread it. Decides inject/skip by presence
|
|
187
|
-
* detection: the block is (re)injected only when it is absent from the working
|
|
188
|
-
* messages — true on the first turn and right after compaction strips it — and
|
|
189
|
-
* skipped on normal cached turns where it already persists in history. This
|
|
190
|
-
* keeps the conversation prefix stable for Anthropic's prefix caching.
|
|
157
|
+
* user tail's content so the assistant sees a workspace grounding block
|
|
158
|
+
* before any other per-turn context.
|
|
191
159
|
*
|
|
192
160
|
* Gating:
|
|
193
161
|
* - `mode === "full"` (skipped in minimal mode).
|
|
194
|
-
* -
|
|
195
|
-
* - no `<workspace>` block is already present in `runMessages`.
|
|
162
|
+
* - `workspaceTopLevelContext` is a non-null, non-empty string.
|
|
196
163
|
*/
|
|
197
164
|
const workspaceContextInjector: Injector = {
|
|
198
165
|
name: "workspace-context",
|
|
199
166
|
order: DEFAULT_INJECTOR_ORDER.workspaceContext,
|
|
200
|
-
async produce(
|
|
201
|
-
ctx: TurnContext,
|
|
202
|
-
runMessages?: Message[],
|
|
203
|
-
): Promise<InjectionBlock | null> {
|
|
167
|
+
async produce(ctx: TurnContext): Promise<InjectionBlock | null> {
|
|
204
168
|
const inputs = readInjectionInputs(ctx);
|
|
205
169
|
const mode = inputs.mode ?? "full";
|
|
206
170
|
if (mode !== "full") return null;
|
|
207
|
-
const text =
|
|
171
|
+
const text = inputs.workspaceTopLevelContext;
|
|
208
172
|
if (!text) return null;
|
|
209
|
-
if (hasInjectedUserTextBlock(runMessages, WORKSPACE_BLOCK_MATCHERS))
|
|
210
|
-
return null;
|
|
211
173
|
return {
|
|
212
174
|
id: "workspace-context",
|
|
213
175
|
text,
|
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
type RuntimeInjectionOptions,
|
|
30
30
|
type RuntimeInjectionResult,
|
|
31
31
|
} from "../../../../daemon/conversation-runtime-assembly.js";
|
|
32
|
-
import { resolveTrustClass } from "../../../../daemon/trust-context.js";
|
|
33
32
|
import { stripHistoricalWebSearchResults } from "../../../../daemon/web-search-history.js";
|
|
34
33
|
import { getLiveGraphMemory } from "../../../../memory/graph/conversation-graph-memory.js";
|
|
35
34
|
import type { PluginLogger } from "../../../../plugin-api/types.js";
|
|
@@ -52,14 +51,15 @@ export interface PostCompactionHookInput {
|
|
|
52
51
|
/**
|
|
53
52
|
* Everything the hook needs in a single context: the loop-supplied
|
|
54
53
|
* {@link PostCompactionHookInput}, the resolved {@link RuntimeInjectionOptions}
|
|
55
|
-
* (spread top-level so each field stays individually addressable), and
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* rather than threaded in.
|
|
54
|
+
* (spread top-level so each field stays individually addressable), and the
|
|
55
|
+
* conversation-scoped state the options bag cannot carry (actor trust and a
|
|
56
|
+
* turn-scoped logger). The memory graph handle is not part of this context —
|
|
57
|
+
* the hook sources it internally via {@link getLiveGraphMemory}.
|
|
60
58
|
*/
|
|
61
59
|
export interface PostCompactContext
|
|
62
60
|
extends RuntimeInjectionOptions, PostCompactionHookInput {
|
|
61
|
+
/** True when the actor for this turn is trusted (guardian-class). */
|
|
62
|
+
isTrustedActor: boolean;
|
|
63
63
|
/** Turn-scoped logger for diagnostics emitted while re-injecting. */
|
|
64
64
|
logger: PluginLogger;
|
|
65
65
|
}
|
|
@@ -67,18 +67,14 @@ export interface PostCompactContext
|
|
|
67
67
|
export default async function postCompactReinject(
|
|
68
68
|
ctx: PostCompactContext,
|
|
69
69
|
): Promise<RuntimeInjectionResult> {
|
|
70
|
-
const { history, logger, ...options } = ctx;
|
|
70
|
+
const { history, isTrustedActor, logger, ...options } = ctx;
|
|
71
71
|
const result = await applyRuntimeInjections(history, options);
|
|
72
72
|
// Re-track the nodes the memory graph last injected so they survive against
|
|
73
73
|
// the re-injected history. Untrusted actors and minimal-mode turns never
|
|
74
74
|
// received a memory-graph injection, so there is nothing to re-track. The
|
|
75
|
-
// actor's trust class is derived from the turn's own trust context (the same
|
|
76
|
-
// value the injector chain resolves), not threaded in from the loop. The
|
|
77
75
|
// live graph handle is looked up from the plugin's own registry by the
|
|
78
76
|
// turn's conversation id — the same instance the turn's retrieval mutated,
|
|
79
77
|
// so re-tracking sees the real cached-node state.
|
|
80
|
-
const isTrustedActor =
|
|
81
|
-
resolveTrustClass(options.turnContext?.trust) === "guardian";
|
|
82
78
|
if (isTrustedActor && options.mode !== "minimal") {
|
|
83
79
|
getLiveGraphMemory(
|
|
84
80
|
options.turnContext?.conversationId,
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
* ordering assumptions about when `defaultInjectors` finishes initializing.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
import { memoryV3Injector } from "../../../memory/v3/shadow-plugin.js";
|
|
21
22
|
import type { Injector } from "../../types.js";
|
|
22
|
-
import {
|
|
23
|
-
import { defaultInjectors } from "./injectors.js";
|
|
23
|
+
import { defaultInjectors } from "../injectors/register.js";
|
|
24
24
|
|
|
25
25
|
let cachedChain: Injector[] | null = null;
|
|
26
26
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { ContextWindowCompactOptions } from "../../../../context/window-manager.js";
|
|
2
|
+
import {
|
|
3
|
+
createInitialReducerState,
|
|
4
|
+
reduceContextOverflow,
|
|
5
|
+
type ReducerState,
|
|
6
|
+
} from "../../../../daemon/context-overflow-reducer.js";
|
|
7
|
+
import type {
|
|
8
|
+
Middleware,
|
|
9
|
+
OverflowReduceArgs,
|
|
10
|
+
OverflowReduceResult,
|
|
11
|
+
} from "../../../types.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Default middleware for the `overflowReduce` pipeline — implements the
|
|
15
|
+
* historical tier-loop semantics (forced compaction, tool-result truncation,
|
|
16
|
+
* media stubbing, injection downgrade) plus the post-step re-injection /
|
|
17
|
+
* re-estimation dance.
|
|
18
|
+
*
|
|
19
|
+
* The middleware intentionally ignores `next`. Overflow reduction is a
|
|
20
|
+
* *terminal* behavior: there is no downstream implementation to defer to when
|
|
21
|
+
* a user-supplied middleware short-circuits. Later plugins may still wrap this
|
|
22
|
+
* one (outer middleware can observe each reduction iteration via their own
|
|
23
|
+
* `next` callback) but the default never delegates to a hypothetical base
|
|
24
|
+
* handler — the inline loop was the base.
|
|
25
|
+
*/
|
|
26
|
+
const defaultOverflowReduceMiddleware: Middleware<
|
|
27
|
+
OverflowReduceArgs,
|
|
28
|
+
OverflowReduceResult
|
|
29
|
+
> = async function defaultOverflowReduceMiddleware(args, _next, _ctx) {
|
|
30
|
+
let messages = args.messages;
|
|
31
|
+
let runMessages = args.runMessages;
|
|
32
|
+
let injectionMode: "full" | "minimal" = "full";
|
|
33
|
+
let reducerState: ReducerState = createInitialReducerState();
|
|
34
|
+
let reducerCompacted = false;
|
|
35
|
+
let attempts = 0;
|
|
36
|
+
|
|
37
|
+
while (attempts < args.maxAttempts && !reducerState.exhausted) {
|
|
38
|
+
// Abort check at the top of every iteration. When the pipeline runner
|
|
39
|
+
// arms a timeout (or the caller aborts externally), `args.abortSignal`
|
|
40
|
+
// is linked to that trigger via `linkAbortSignal`, so this check lets
|
|
41
|
+
// us bail out BETWEEN iterations rather than letting another round of
|
|
42
|
+
// compaction / re-injection mutate `ctx.messages` after the turn has
|
|
43
|
+
// already failed. Individual `reduceContextOverflow` calls also honor
|
|
44
|
+
// the signal, but without this gate a fresh iteration could still
|
|
45
|
+
// start after the signal fires, since the previous one returned
|
|
46
|
+
// normally before the abort propagated.
|
|
47
|
+
args.abortSignal?.throwIfAborted();
|
|
48
|
+
|
|
49
|
+
attempts++;
|
|
50
|
+
args.emitActivityState();
|
|
51
|
+
|
|
52
|
+
const basisMessages = messages;
|
|
53
|
+
const step = await reduceContextOverflow(
|
|
54
|
+
basisMessages,
|
|
55
|
+
{
|
|
56
|
+
providerName: args.providerName,
|
|
57
|
+
systemPrompt: args.systemPrompt,
|
|
58
|
+
contextWindow: args.contextWindow,
|
|
59
|
+
targetTokens: args.preflightBudget,
|
|
60
|
+
toolTokenBudget: args.toolTokenBudget,
|
|
61
|
+
},
|
|
62
|
+
reducerState,
|
|
63
|
+
(msgs, signal, opts: ContextWindowCompactOptions) =>
|
|
64
|
+
args.compactFn(msgs, signal, opts),
|
|
65
|
+
args.abortSignal,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
reducerState = step.state;
|
|
69
|
+
messages = step.messages;
|
|
70
|
+
injectionMode = step.state.injectionMode;
|
|
71
|
+
|
|
72
|
+
// Per-iteration compaction flag: whether THIS step just produced a
|
|
73
|
+
// fresh compaction. PKB / NOW re-injection is gated on this — see the
|
|
74
|
+
// reinjectForMode JSDoc for why the two signals differ.
|
|
75
|
+
const stepCompacted = step.compactionResult?.compacted === true;
|
|
76
|
+
|
|
77
|
+
// Let the orchestrator apply compaction side effects (circuit-breaker
|
|
78
|
+
// tracking, event emission, ctx mutation) before we re-inject.
|
|
79
|
+
if (step.compactionResult) {
|
|
80
|
+
await args.onCompactionResult(step.compactionResult, basisMessages);
|
|
81
|
+
if (stepCompacted) {
|
|
82
|
+
reducerCompacted = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Second abort gate — if the side effects or the step itself took us
|
|
87
|
+
// past the deadline, don't rebuild runMessages or iterate again.
|
|
88
|
+
args.abortSignal?.throwIfAborted();
|
|
89
|
+
|
|
90
|
+
// Rebuild runMessages via the orchestrator-supplied helper (which
|
|
91
|
+
// re-runs `applyRuntimeInjections` with potentially downgraded mode
|
|
92
|
+
// and freshly re-hydrated PKB/NOW blocks after compaction). We pass
|
|
93
|
+
// the current reduced `messages` explicitly so the orchestrator never
|
|
94
|
+
// has to read from mutable shared state to rebuild runMessages — a
|
|
95
|
+
// tier that doesn't trigger compaction (tool-result truncation, media
|
|
96
|
+
// stubbing) won't update `ctx.messages` on its own.
|
|
97
|
+
//
|
|
98
|
+
// `stepCompacted` and `reducerCompacted` are both passed so the
|
|
99
|
+
// orchestrator can gate PKB / NOW re-injection per-iteration while
|
|
100
|
+
// keeping `slackChronologicalMessages` suppressed once any iteration
|
|
101
|
+
// has compacted.
|
|
102
|
+
runMessages = await args.reinjectForMode(
|
|
103
|
+
messages,
|
|
104
|
+
injectionMode,
|
|
105
|
+
stepCompacted,
|
|
106
|
+
reducerCompacted,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// Re-estimate with injections included — `step.estimatedTokens` was
|
|
110
|
+
// computed on bare history and doesn't account for tokens added by
|
|
111
|
+
// runtime injections.
|
|
112
|
+
const postInjectionTokens = args.estimatePostInjection(runMessages);
|
|
113
|
+
if (postInjectionTokens <= args.preflightBudget) break;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
messages,
|
|
118
|
+
runMessages,
|
|
119
|
+
injectionMode,
|
|
120
|
+
reducerState,
|
|
121
|
+
reducerCompacted,
|
|
122
|
+
attempts,
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export default defaultOverflowReduceMiddleware;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "default-overflow-reduce",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "First-party default plugin wrapping the assistant's built-in overflow-reduce pipeline with a passthrough implementation.",
|
|
5
|
+
"private": true,
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./register.ts",
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=20.12.0"
|
|
11
|
+
},
|
|
12
|
+
"peerDependencies": {
|
|
13
|
+
"@vellumai/plugin-api": "^0.8.0"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default `overflowReduce` plugin — extracted verbatim from the inline
|
|
3
|
+
* preflight reducer loop that previously lived in
|
|
4
|
+
* `daemon/conversation-agent-loop.ts` (the `while (preflightAttempts < …)`
|
|
5
|
+
* block around lines 1045–1156 before PR 23).
|
|
6
|
+
*
|
|
7
|
+
* The plugin owns the reducer tier-loop (forced compaction, tool-result
|
|
8
|
+
* truncation, media stubbing, injection downgrade) and the post-step
|
|
9
|
+
* re-injection / re-estimation dance. Orchestrator-specific coupling
|
|
10
|
+
* (activity emission, circuit-breaker tracking, compaction-result
|
|
11
|
+
* application, runtime injection reassembly) is threaded in through the
|
|
12
|
+
* callbacks carried on {@link OverflowReduceArgs}; the plugin itself has no
|
|
13
|
+
* access to the agent-loop context object.
|
|
14
|
+
*
|
|
15
|
+
* The forced-compaction tier runs through the orchestrator-supplied
|
|
16
|
+
* `compactFn`, which routes into the `compaction` plugin pipeline so
|
|
17
|
+
* registered compaction middleware observes reducer-initiated invocations
|
|
18
|
+
* alongside the orchestrator-owned call sites. Non-compaction tiers
|
|
19
|
+
* (tool-result truncation, media stubbing, injection downgrade) remain
|
|
20
|
+
* in-process: they mutate message arrays directly without crossing a
|
|
21
|
+
* pipeline boundary. The reducer itself runs under the `overflowReduce`
|
|
22
|
+
* pipeline, so the full layering is `overflowReduce` → reducer tier loop
|
|
23
|
+
* → (for the forced-compaction tier only) nested `compaction` pipeline.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { type Plugin } from "../../types.js";
|
|
27
|
+
import defaultOverflowReduceMiddleware from "./middlewares/overflowReduce.js";
|
|
28
|
+
import pkg from "./package.json" with { type: "json" };
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The default plugin registered at bootstrap. No `init`/`onShutdown` —
|
|
32
|
+
* registering the middleware is the only behavior.
|
|
33
|
+
*/
|
|
34
|
+
export const defaultOverflowReducePlugin: Plugin = {
|
|
35
|
+
manifest: {
|
|
36
|
+
name: pkg.name,
|
|
37
|
+
version: pkg.version,
|
|
38
|
+
},
|
|
39
|
+
middleware: {
|
|
40
|
+
overflowReduce: defaultOverflowReduceMiddleware,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
* absolute paths or workspace-local copies — the TypeScript compiler erases
|
|
32
32
|
* them and they have no module-identity effect at runtime.
|
|
33
33
|
*
|
|
34
|
-
* See `
|
|
35
|
-
* `assistant/examples/plugins/echo
|
|
34
|
+
* See `assistant/docs/plugins.md` for the full authoring contract and
|
|
35
|
+
* `assistant/examples/plugins/echo/register.ts` for a worked example.
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
38
|
import { assistantEventHub } from "../runtime/assistant-event-hub.js";
|
package/src/plugins/pipeline.ts
CHANGED
|
@@ -1,16 +1,297 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Plugin
|
|
2
|
+
* Plugin pipeline runner.
|
|
3
3
|
*
|
|
4
|
-
* A "
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* A "pipeline" is a named chain of {@link Middleware}s wrapped around a
|
|
5
|
+
* terminal handler (the original behavior that existed before plugins). The
|
|
6
|
+
* runner composes the chain in onion order, runs it under an optional
|
|
7
|
+
* per-pipeline timeout, and emits one structured log record per invocation
|
|
8
|
+
* covering success, error, and timeout uniformly.
|
|
8
9
|
*
|
|
9
|
-
* Design doc: `.private/plans/agent-plugin-system.md
|
|
10
|
+
* Design doc: `.private/plans/agent-plugin-system.md` (PR 12 notes).
|
|
11
|
+
*
|
|
12
|
+
* Semantics:
|
|
13
|
+
* - Onion composition — the first element of `middlewares` is the outermost
|
|
14
|
+
* wrapper; it sees the request first and the response last. Terminal runs
|
|
15
|
+
* in the middle.
|
|
16
|
+
* - Strict-fail — there is NO `try/catch` around user middleware. Errors,
|
|
17
|
+
* including `PluginTimeoutError` from a breached budget, propagate to the
|
|
18
|
+
* caller. The `finally` block only handles logging, never error recovery.
|
|
19
|
+
* - Timeout — when `timeoutMs` is a finite number, the invocation races a
|
|
20
|
+
* timer. A timeout rejection is a `PluginTimeoutError` carrying the
|
|
21
|
+
* pipeline name, the best-known offending plugin (from `ctx.pluginName`),
|
|
22
|
+
* and elapsed ms. When `timeoutMs` is `null`/`undefined`, no timer is
|
|
23
|
+
* armed — pipelines like `compaction` rely on downstream timeouts instead.
|
|
10
24
|
*/
|
|
11
25
|
|
|
26
|
+
import type { Logger } from "pino";
|
|
27
|
+
|
|
12
28
|
import type { HookName } from "../plugin-api/constants.js";
|
|
29
|
+
import { getLogger } from "../util/logger.js";
|
|
13
30
|
import { getHooksFor } from "./registry.js";
|
|
31
|
+
import {
|
|
32
|
+
type Middleware,
|
|
33
|
+
type PipelineName,
|
|
34
|
+
PluginTimeoutError,
|
|
35
|
+
type TurnContext,
|
|
36
|
+
} from "./types.js";
|
|
37
|
+
|
|
38
|
+
const moduleLogger = getLogger("plugin-pipeline");
|
|
39
|
+
|
|
40
|
+
// ─── Default timeouts ───────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Default per-pipeline timeout budgets in milliseconds. A value of `null`
|
|
44
|
+
* means the runner does NOT arm a timer — the pipeline relies on its
|
|
45
|
+
* downstream for budgeting (e.g. `compaction` defers to the provider-level
|
|
46
|
+
* HTTP timeouts of the summary call it wraps).
|
|
47
|
+
*
|
|
48
|
+
* Callers pass the appropriate entry as `runPipeline`'s `timeoutMs` argument.
|
|
49
|
+
* The design doc locks these numbers in; do not tweak without coordinating
|
|
50
|
+
* a design update.
|
|
51
|
+
*/
|
|
52
|
+
export const DEFAULT_TIMEOUTS: Record<PipelineName, number | null> = {
|
|
53
|
+
compaction: null,
|
|
54
|
+
overflowReduce: null,
|
|
55
|
+
circuitBreaker: null,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// ─── Composition ────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Compose an ordered list of {@link Middleware}s around a terminal handler
|
|
62
|
+
* using onion semantics. The first element of `middlewares` is the outermost
|
|
63
|
+
* layer.
|
|
64
|
+
*
|
|
65
|
+
* The returned function accepts `(args, ctx)` and runs the entire chain:
|
|
66
|
+
* ```
|
|
67
|
+
* middlewares[0] → middlewares[1] → ... → terminal → ... → middlewares[1] → middlewares[0]
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* Middlewares that never call `next` short-circuit the chain — the terminal
|
|
71
|
+
* and any deeper middleware are never invoked. Middlewares that throw abort
|
|
72
|
+
* the chain; the error flows back out through any outer middleware unchanged
|
|
73
|
+
* (no internal try/catch).
|
|
74
|
+
*/
|
|
75
|
+
export function composeMiddleware<A, R>(
|
|
76
|
+
middlewares: ReadonlyArray<Middleware<A, R>>,
|
|
77
|
+
terminal: (args: A) => Promise<R>,
|
|
78
|
+
): (args: A, ctx: TurnContext) => Promise<R> {
|
|
79
|
+
return (args, ctx) => {
|
|
80
|
+
// Walk back-to-front, wrapping each middleware around `next`. The last
|
|
81
|
+
// middleware in the array wraps `terminal`; earlier entries wrap the
|
|
82
|
+
// accumulated chain so they execute first.
|
|
83
|
+
let next: (args: A) => Promise<R> = terminal;
|
|
84
|
+
for (let i = middlewares.length - 1; i >= 0; i--) {
|
|
85
|
+
const mw = middlewares[i]!;
|
|
86
|
+
const downstream = next;
|
|
87
|
+
next = (innerArgs: A) => mw(innerArgs, downstream, ctx);
|
|
88
|
+
}
|
|
89
|
+
return next(args);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── Abort-signal linking ───────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Return a shallow-cloned `args` object where every `AbortSignal`-typed
|
|
97
|
+
* top-level property is swapped for a signal linked to `internalController`.
|
|
98
|
+
*
|
|
99
|
+
* "Linked" here means: the returned signal aborts when either the caller's
|
|
100
|
+
* original signal aborts OR `internalController` aborts (e.g. the pipeline
|
|
101
|
+
* timer fires). The caller's args are never mutated.
|
|
102
|
+
*
|
|
103
|
+
* When `args` carries no `AbortSignal` property, the original object is
|
|
104
|
+
* returned unchanged — pipelines whose terminals don't consume a signal
|
|
105
|
+
* (e.g. `circuitBreaker`) see identical behavior to before.
|
|
106
|
+
* The return value's `cleanup()` tears down any `addEventListener("abort",
|
|
107
|
+
* ...)` handlers attached to the caller's signal so a pipeline that
|
|
108
|
+
* completes successfully doesn't leak listeners on the caller's controller.
|
|
109
|
+
*/
|
|
110
|
+
function linkAbortSignal<A>(
|
|
111
|
+
args: A,
|
|
112
|
+
internalController: AbortController,
|
|
113
|
+
): { args: A; cleanup: () => void } {
|
|
114
|
+
if (args === null || typeof args !== "object") {
|
|
115
|
+
return { args, cleanup: () => {} };
|
|
116
|
+
}
|
|
117
|
+
const abortListeners: Array<{
|
|
118
|
+
signal: AbortSignal;
|
|
119
|
+
listener: () => void;
|
|
120
|
+
}> = [];
|
|
121
|
+
const record = args as Record<string, unknown>;
|
|
122
|
+
const patched: Record<string, unknown> = { ...record };
|
|
123
|
+
let swappedAny = false;
|
|
124
|
+
for (const key of Object.keys(record)) {
|
|
125
|
+
const value = record[key];
|
|
126
|
+
if (value instanceof AbortSignal) {
|
|
127
|
+
swappedAny = true;
|
|
128
|
+
if (value.aborted) {
|
|
129
|
+
// Caller already aborted — propagate immediately so the inner call
|
|
130
|
+
// sees the abort without waiting for the listener to fire.
|
|
131
|
+
internalController.abort();
|
|
132
|
+
} else {
|
|
133
|
+
const listener = () => internalController.abort();
|
|
134
|
+
value.addEventListener("abort", listener, { once: true });
|
|
135
|
+
abortListeners.push({ signal: value, listener });
|
|
136
|
+
}
|
|
137
|
+
patched[key] = internalController.signal;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const cleanup = () => {
|
|
141
|
+
for (const { signal, listener } of abortListeners) {
|
|
142
|
+
signal.removeEventListener("abort", listener);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
return { args: (swappedAny ? patched : args) as A, cleanup };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ─── Runner ─────────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
type PipelineLogRecord = {
|
|
151
|
+
event: "plugin.pipeline";
|
|
152
|
+
pipeline: PipelineName;
|
|
153
|
+
chain: ReadonlyArray<string>;
|
|
154
|
+
durationMs: number;
|
|
155
|
+
outcome: "success" | "error" | "timeout";
|
|
156
|
+
pluginName?: string;
|
|
157
|
+
errorName?: string;
|
|
158
|
+
errorMessage?: string;
|
|
159
|
+
errorStack?: string;
|
|
160
|
+
timeoutMs?: number;
|
|
161
|
+
requestId: string;
|
|
162
|
+
conversationId: string;
|
|
163
|
+
turnIndex?: number;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Best-effort detection of a pino-compatible logger on the `TurnContext`.
|
|
168
|
+
*
|
|
169
|
+
* `TurnContext` intentionally doesn't require a logger slot — most code paths
|
|
170
|
+
* use the module-level logger. Some callers (notably plugin-scoped pipelines
|
|
171
|
+
* in later PRs) may attach a child logger via `(ctx as any).logger`. We type
|
|
172
|
+
* the field loosely to avoid coupling `TurnContext` to pino.
|
|
173
|
+
*/
|
|
174
|
+
function selectLogger(ctx: TurnContext): Logger {
|
|
175
|
+
const maybe = (ctx as { logger?: unknown }).logger;
|
|
176
|
+
if (maybe && typeof maybe === "object" && "info" in maybe) {
|
|
177
|
+
return maybe as Logger;
|
|
178
|
+
}
|
|
179
|
+
return moduleLogger;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Execute a named pipeline: compose middleware, run it under an optional
|
|
184
|
+
* timeout, and emit one structured log record covering success, error, or
|
|
185
|
+
* timeout uniformly.
|
|
186
|
+
*
|
|
187
|
+
* @param name The pipeline identifier. Used for telemetry and error
|
|
188
|
+
* attribution.
|
|
189
|
+
* @param middlewares Ordered list of middlewares to wrap around `terminal`.
|
|
190
|
+
* Names for the log `chain` are pulled from `Function.name`
|
|
191
|
+
* (empty strings are kept so length always matches).
|
|
192
|
+
* @param terminal The original behavior. Runs when all middlewares call
|
|
193
|
+
* `next`.
|
|
194
|
+
* @param args Pipeline-specific input payload.
|
|
195
|
+
* @param ctx Per-turn context. May carry an optional `logger` slot;
|
|
196
|
+
* otherwise the module logger is used.
|
|
197
|
+
* @param timeoutMs Deadline in milliseconds. `null`/`undefined` disables
|
|
198
|
+
* the timer entirely (inherits downstream timeouts).
|
|
199
|
+
*
|
|
200
|
+
* @throws {PluginTimeoutError} When `timeoutMs` is non-null and the chain
|
|
201
|
+
* exceeds the budget. `ctx.pluginName` (if set) is attached as the
|
|
202
|
+
* offending plugin.
|
|
203
|
+
* @throws Any error produced by user middleware or the terminal — flows
|
|
204
|
+
* through unchanged.
|
|
205
|
+
*/
|
|
206
|
+
export async function runPipeline<A, R>(
|
|
207
|
+
name: PipelineName,
|
|
208
|
+
middlewares: ReadonlyArray<Middleware<A, R>>,
|
|
209
|
+
terminal: (args: A) => Promise<R>,
|
|
210
|
+
args: A,
|
|
211
|
+
ctx: TurnContext,
|
|
212
|
+
timeoutMs?: number | null,
|
|
213
|
+
): Promise<R> {
|
|
214
|
+
const logger = selectLogger(ctx);
|
|
215
|
+
const chain = middlewares.map((m) => m.name || "anonymous");
|
|
216
|
+
const composed = composeMiddleware(middlewares, terminal);
|
|
217
|
+
const start = performance.now();
|
|
218
|
+
|
|
219
|
+
let outcome: "success" | "error" | "timeout" = "success";
|
|
220
|
+
let thrown: unknown = undefined;
|
|
221
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
if (typeof timeoutMs === "number" && Number.isFinite(timeoutMs)) {
|
|
225
|
+
const budget = timeoutMs;
|
|
226
|
+
// Internal controller: fires on either (a) the timer, so the inner call
|
|
227
|
+
// actually observes the budget breach instead of running forever after
|
|
228
|
+
// `Promise.race` rejects; or (b) the caller's own signal, so external
|
|
229
|
+
// cancellation still reaches the inner call transparently. Any
|
|
230
|
+
// `AbortSignal`-typed property on `args` (e.g. `signal` on
|
|
231
|
+
// `CompactionArgs`, `abortSignal` on `OverflowReduceArgs`) is swapped
|
|
232
|
+
// for this linked signal on a shallow-cloned args object — we never
|
|
233
|
+
// mutate the caller's args.
|
|
234
|
+
const internalController = new AbortController();
|
|
235
|
+
const { args: effectiveArgs, cleanup } = linkAbortSignal(
|
|
236
|
+
args,
|
|
237
|
+
internalController,
|
|
238
|
+
);
|
|
239
|
+
const timeoutPromise = new Promise<never>((_resolve, reject) => {
|
|
240
|
+
timer = setTimeout(() => {
|
|
241
|
+
internalController.abort();
|
|
242
|
+
reject(
|
|
243
|
+
new PluginTimeoutError(
|
|
244
|
+
name,
|
|
245
|
+
ctx.pluginName,
|
|
246
|
+
Math.round(performance.now() - start),
|
|
247
|
+
),
|
|
248
|
+
);
|
|
249
|
+
}, budget);
|
|
250
|
+
});
|
|
251
|
+
try {
|
|
252
|
+
return await Promise.race([
|
|
253
|
+
composed(effectiveArgs, ctx),
|
|
254
|
+
timeoutPromise,
|
|
255
|
+
]);
|
|
256
|
+
} finally {
|
|
257
|
+
cleanup();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return await composed(args, ctx);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
thrown = err;
|
|
263
|
+
outcome = err instanceof PluginTimeoutError ? "timeout" : "error";
|
|
264
|
+
throw err;
|
|
265
|
+
} finally {
|
|
266
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
267
|
+
const durationMs = Math.round(performance.now() - start);
|
|
268
|
+
const record: PipelineLogRecord = {
|
|
269
|
+
event: "plugin.pipeline",
|
|
270
|
+
pipeline: name,
|
|
271
|
+
chain,
|
|
272
|
+
durationMs,
|
|
273
|
+
outcome,
|
|
274
|
+
requestId: ctx.requestId,
|
|
275
|
+
conversationId: ctx.conversationId,
|
|
276
|
+
};
|
|
277
|
+
if (ctx.turnIndex !== undefined) record.turnIndex = ctx.turnIndex;
|
|
278
|
+
if (ctx.pluginName !== undefined) record.pluginName = ctx.pluginName;
|
|
279
|
+
if (typeof timeoutMs === "number" && Number.isFinite(timeoutMs)) {
|
|
280
|
+
record.timeoutMs = timeoutMs;
|
|
281
|
+
}
|
|
282
|
+
if (thrown !== undefined) {
|
|
283
|
+
if (thrown instanceof Error) {
|
|
284
|
+
record.errorName = thrown.name;
|
|
285
|
+
record.errorMessage = thrown.message;
|
|
286
|
+
if (thrown.stack) record.errorStack = thrown.stack;
|
|
287
|
+
} else {
|
|
288
|
+
record.errorName = "NonError";
|
|
289
|
+
record.errorMessage = String(thrown);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
logger.info(record, "plugin.pipeline");
|
|
293
|
+
}
|
|
294
|
+
}
|
|
14
295
|
|
|
15
296
|
// ─── Hook runner ────────────────────────────────────────────────────────────
|
|
16
297
|
|
|
@@ -21,6 +302,12 @@ import { getHooksFor } from "./registry.js";
|
|
|
21
302
|
* that replaces the threaded value for subsequent hooks. The final context
|
|
22
303
|
* after the chain settles is returned.
|
|
23
304
|
*
|
|
305
|
+
* `runHook` is the hook-side counterpart to {@link runPipeline}:
|
|
306
|
+
* `runPipeline` composes middleware around a terminal handler for stateful
|
|
307
|
+
* request/response pipelines (compaction, overflow reduction, etc.);
|
|
308
|
+
* `runHook` walks ordered hook functions for declarative chain-style
|
|
309
|
+
* context transformations (`user-prompt-submit`, `post-tool-use`).
|
|
310
|
+
*
|
|
24
311
|
* @param name The hook identifier — pick one from {@link HOOKS}.
|
|
25
312
|
* @param initialCtx Context the first hook receives.
|
|
26
313
|
* @returns The final context after the chain settles. Same reference as
|