@vellumai/assistant 0.10.3 → 0.10.4-staging.1
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/openapi.yaml +73 -56
- package/package.json +1 -1
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
- package/src/__tests__/assistant-stream-state.test.ts +3 -76
- package/src/__tests__/background-workers-disk-pressure.test.ts +4 -2
- package/src/__tests__/channel-approval-routes.test.ts +21 -26
- package/src/__tests__/channel-delivery-store.test.ts +28 -0
- package/src/__tests__/channel-guardian.test.ts +82 -32
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
- package/src/__tests__/channel-reply-delivery.test.ts +6 -2
- package/src/__tests__/compaction-ledger-store.test.ts +128 -0
- package/src/__tests__/config-loader-backfill.test.ts +148 -0
- package/src/__tests__/consult-deadline.test.ts +60 -0
- package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
- package/src/__tests__/contact-store-user-file.test.ts +7 -10
- package/src/__tests__/contacts-relay-reads.test.ts +6 -9
- package/src/__tests__/contacts-write.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -2
- package/src/__tests__/conversation-agent-loop.test.ts +98 -7
- package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
- package/src/__tests__/conversation-error.test.ts +18 -0
- package/src/__tests__/conversation-fork-crud.test.ts +354 -24
- package/src/__tests__/conversation-title-service.test.ts +222 -201
- package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
- package/src/__tests__/delete-propagation.test.ts +5 -3
- package/src/__tests__/dm-backfill.test.ts +6 -4
- package/src/__tests__/emit-signal-routing-intent.test.ts +2 -6
- package/src/__tests__/guardian-binding-drift-heal.test.ts +43 -23
- package/src/__tests__/guardian-dispatch.test.ts +50 -5
- package/src/__tests__/guardian-routing-state.test.ts +6 -10
- package/src/__tests__/helpers/channel-test-adapter.ts +45 -12
- package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
- package/src/__tests__/helpers/mock-logger.ts +1 -0
- package/src/__tests__/helpers/seed-contact-channel.ts +96 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +87 -10
- package/src/__tests__/invite-redemption-service.test.ts +273 -53
- package/src/__tests__/invite-routes-http.test.ts +34 -0
- package/src/__tests__/invite-service-ipc.test.ts +65 -2
- package/src/__tests__/list-messages-page-latest.test.ts +173 -4
- package/src/__tests__/mcp-config-secret-boundary.test.ts +3 -0
- package/src/__tests__/non-member-access-request.test.ts +15 -13
- package/src/__tests__/onboarding-persona-write.test.ts +52 -22
- package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
- package/src/__tests__/persona-resolver.test.ts +75 -45
- package/src/__tests__/plugin-bootstrap.test.ts +13 -5
- package/src/__tests__/plugin-disabled-state.test.ts +190 -0
- package/src/__tests__/provider-usage-tracking.test.ts +1 -1
- package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
- package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
- package/src/__tests__/reaction-persistence.test.ts +51 -4
- package/src/__tests__/relay-server.test.ts +88 -31
- package/src/__tests__/runtime-attachment-metadata.test.ts +9 -11
- package/src/__tests__/settings-routes.test.ts +32 -0
- package/src/__tests__/slack-block-formatting.test.ts +1 -38
- package/src/__tests__/sse-actor-principal-guardian-source.test.ts +13 -36
- package/src/__tests__/stt-hints.test.ts +6 -3
- package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
- package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
- package/src/__tests__/subagent-role-registry.test.ts +17 -4
- package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
- package/src/__tests__/subagent-tools.test.ts +398 -3
- package/src/__tests__/thread-backfill.test.ts +3 -3
- package/src/__tests__/tool-preview-lifecycle.test.ts +26 -10
- package/src/__tests__/tool-start-timestamp.test.ts +4 -3
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -2
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
- package/src/__tests__/trusted-contact-multichannel.test.ts +16 -7
- package/src/__tests__/trusted-contact-verification.test.ts +79 -54
- package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
- package/src/__tests__/voice-invite-redemption.test.ts +183 -20
- package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
- package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +2 -2
- package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +35 -47
- package/src/agent/loop-exclusive-tool.test.ts +19 -15
- package/src/agent/loop-native-web-search.test.ts +200 -0
- package/src/agent/loop.ts +108 -1
- package/src/api/responses/conversation-message.ts +9 -0
- package/src/approvals/guardian-request-resolvers.ts +16 -4
- package/src/calls/__tests__/relay-setup-router.test.ts +10 -18
- package/src/calls/guardian-dispatch.ts +14 -11
- package/src/calls/inbound-trust-reader.ts +7 -1
- package/src/calls/relay-access-wait.ts +6 -6
- package/src/calls/relay-server.ts +22 -2
- package/src/calls/relay-setup-router.ts +10 -10
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
- package/src/cli/commands/contacts.ts +10 -7
- package/src/cli/commands/memory/__tests__/worker.test.ts +147 -17
- package/src/cli/commands/memory/worker.ts +97 -30
- package/src/cli/commands/plugins.ts +3 -146
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +17 -17
- package/src/cli/lib/__tests__/publish-plugin.test.ts +98 -0
- package/src/cli/lib/publish-plugin.ts +231 -1
- package/src/config/__tests__/sync-gated-profiles.test.ts +5 -7
- package/src/config/bundled-skills/subagent/SKILL.md +16 -1
- package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
- package/src/config/call-site-defaults.ts +0 -6
- package/src/config/llm-resolver.ts +0 -3
- package/src/config/schemas/call-site-catalog.ts +0 -7
- package/src/config/schemas/heartbeat.ts +2 -5
- package/src/config/schemas/llm.ts +3 -12
- package/src/config/schemas/memory-lifecycle.ts +1 -1
- package/src/config/seed-inference-profiles.ts +76 -35
- package/src/config/sync-gated-profiles.ts +0 -3
- package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +7 -8
- package/src/contacts/__tests__/member-write-relay.test.ts +35 -11
- package/src/contacts/contact-store.ts +27 -237
- package/src/contacts/contacts-write.ts +18 -58
- package/src/contacts/gateway-channel-read.ts +51 -0
- package/src/contacts/member-write-relay.ts +25 -31
- package/src/contacts/types.ts +3 -15
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
- package/src/daemon/conversation-agent-loop-handlers.ts +29 -10
- package/src/daemon/conversation-agent-loop.ts +68 -61
- package/src/daemon/conversation-error.ts +7 -10
- package/src/daemon/conversation-tool-setup.ts +0 -10
- package/src/daemon/conversation.ts +10 -0
- package/src/daemon/external-plugins-bootstrap.ts +8 -2
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -2
- package/src/daemon/handlers/__tests__/config-channels.test.ts +9 -14
- package/src/daemon/handlers/config-channels.ts +14 -29
- package/src/daemon/lifecycle.ts +16 -4
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/heartbeat/heartbeat-service.ts +5 -0
- package/src/home/relationship-state-writer.ts +5 -0
- package/src/memory/__tests__/embedding-cache.test.ts +136 -0
- package/src/memory/compaction-ledger-store.ts +107 -0
- package/src/memory/conversation-crud.ts +136 -61
- package/src/memory/conversation-title-service.ts +173 -24
- package/src/memory/embedding-backend.ts +8 -1
- package/src/memory/embedding-cache.ts +139 -0
- package/src/memory/jobs-worker.ts +75 -29
- package/src/memory/memory-retrospective-job.ts +5 -0
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +27 -5
- package/src/memory/migrations/302-create-compaction-events.ts +107 -0
- package/src/memory/migrations/303-add-conversation-creation-seq.ts +33 -0
- package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +79 -6
- package/src/memory/schema/contacts.ts +6 -2
- package/src/memory/schema/conversations.ts +39 -0
- package/src/memory/steps.ts +1090 -367
- package/src/memory/worker-control.ts +104 -18
- package/src/memory/worker-process.ts +17 -0
- package/src/messaging/channel-binding-metadata.ts +31 -0
- package/src/messaging/channel-binding-schema.ts +51 -0
- package/src/messaging/providers/__tests__/callback-routing.test.ts +45 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +195 -0
- package/src/messaging/providers/a2a/__tests__/deliver.test.ts +11 -0
- package/src/messaging/providers/a2a/deliver.ts +5 -1
- package/src/messaging/providers/a2a/transport.ts +10 -0
- package/src/messaging/providers/callback-routing.ts +48 -0
- package/src/messaging/providers/channel-transport.ts +55 -0
- package/src/messaging/providers/index.ts +65 -241
- package/src/messaging/providers/slack/binding-metadata.ts +62 -0
- package/src/messaging/providers/slack/transport.ts +92 -0
- package/src/messaging/providers/telegram-bot/transport.ts +51 -0
- package/src/messaging/providers/whatsapp/transport.ts +38 -0
- package/src/notifications/__tests__/broadcaster.test.ts +0 -8
- package/src/notifications/__tests__/connected-channels.test.ts +8 -36
- package/src/notifications/__tests__/destination-resolver.test.ts +12 -117
- package/src/notifications/destination-resolver.ts +7 -23
- package/src/notifications/emit-signal.ts +5 -11
- package/src/plugins/defaults/index.ts +0 -35
- package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +11 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +243 -2
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +167 -14
- package/src/plugins/disabled-state.ts +31 -0
- package/src/plugins/registry.ts +55 -12
- package/src/prompts/persona-resolver.ts +43 -11
- package/src/providers/call-site-routing.ts +41 -0
- package/src/providers/provider-send-message.ts +6 -0
- package/src/providers/ratelimit.ts +6 -0
- package/src/providers/registry.ts +1 -1
- package/src/providers/retry.ts +6 -0
- package/src/providers/types.ts +13 -0
- package/src/providers/usage-tracking.ts +6 -0
- package/src/runtime/__tests__/guardian-vellum-migration.test.ts +30 -27
- package/src/runtime/__tests__/local-principal-trust.test.ts +16 -18
- package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
- package/src/runtime/__tests__/trust-verdict-consumer.test.ts +115 -168
- package/src/runtime/access-request-helper.ts +1 -2
- package/src/runtime/actor-trust-resolver.ts +44 -17
- package/src/runtime/anchored-guardian.test.ts +7 -54
- package/src/runtime/anchored-guardian.ts +4 -53
- package/src/runtime/assistant-stream-state.ts +12 -74
- package/src/runtime/channel-reply-delivery.ts +3 -8
- package/src/runtime/guardian-vellum-migration.ts +18 -16
- package/src/runtime/invite-redemption-service.ts +25 -10
- package/src/runtime/local-actor-identity.test.ts +108 -0
- package/src/runtime/local-actor-identity.ts +27 -20
- package/src/runtime/member-verdict-cache.ts +0 -0
- package/src/runtime/routes/__tests__/contact-routes.test.ts +100 -7
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +1 -2
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +2 -1
- package/src/runtime/routes/contact-routes.ts +40 -25
- package/src/runtime/routes/conversation-list-routes.ts +1 -29
- package/src/runtime/routes/conversation-routes.ts +27 -7
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -10
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -8
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +19 -0
- package/src/runtime/routes/settings-routes.ts +8 -3
- package/src/runtime/services/conversation-serializer.ts +6 -49
- package/src/runtime/slack-block-formatting.ts +0 -15
- package/src/runtime/trust-verdict-consumer.ts +36 -41
- package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
- package/src/{plugins/defaults/advisor/__tests__/transcript.test.ts → subagent/__tests__/consult-transcript.test.ts} +47 -10
- package/src/{plugins/defaults/advisor/steering.ts → subagent/consult-prompt.ts} +17 -39
- package/src/{plugins/defaults/advisor/transcript.ts → subagent/consult-transcript.ts} +18 -8
- package/src/subagent/index.ts +1 -1
- package/src/subagent/manager.ts +245 -33
- package/src/subagent/types.ts +8 -1
- package/src/tools/registry.ts +10 -3
- package/src/tools/subagent/consult-deadline.ts +49 -0
- package/src/tools/subagent/spawn.ts +234 -5
- package/src/util/logger.ts +9 -0
- package/src/util/platform.ts +14 -0
- package/src/workspace/migrations/031-drop-user-md.ts +232 -148
- package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
- package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
- package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -314
- package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +0 -106
- package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +0 -60
- package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
- package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
- package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
- package/src/plugins/defaults/advisor/config.ts +0 -21
- package/src/plugins/defaults/advisor/consult.ts +0 -197
- package/src/plugins/defaults/advisor/context-pack.ts +0 -288
- package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
- package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
- package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
- package/src/plugins/defaults/advisor/package.json +0 -14
- package/src/plugins/defaults/advisor/tools/advisor.ts +0 -92
package/src/subagent/manager.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { wrapWithCallSiteRouting } from "../providers/call-site-routing.js";
|
|
|
24
24
|
import { resolveDefaultProvider } from "../providers/connection-resolution.js";
|
|
25
25
|
import { RateLimitProvider } from "../providers/ratelimit.js";
|
|
26
26
|
import { listProviders } from "../providers/registry.js";
|
|
27
|
+
import type { Message, TextContent } from "../providers/types.js";
|
|
27
28
|
import { createAbortReason } from "../util/abort-reasons.js";
|
|
28
29
|
import { ProviderNotConfiguredError } from "../util/errors.js";
|
|
29
30
|
import { getLogger } from "../util/logger.js";
|
|
@@ -58,6 +59,38 @@ export function mergeSkillIds(
|
|
|
58
59
|
return [...new Set([...roleSkillIds, ...(configSkillIds ?? [])])];
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
// ── Final-text extraction helper ────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Concatenate the `text` blocks of the conversation's trailing assistant
|
|
66
|
+
* message. Used by `spawnAndAwait` to return the child's final synthesis to
|
|
67
|
+
* the awaiting caller. Returns an empty string when the conversation has no
|
|
68
|
+
* assistant message or the final assistant message carries no text blocks
|
|
69
|
+
* (e.g. it ended on a tool_use).
|
|
70
|
+
*/
|
|
71
|
+
function extractFinalAssistantText(messages: Message[]): string {
|
|
72
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
73
|
+
const message = messages[i];
|
|
74
|
+
if (message.role !== "assistant") continue;
|
|
75
|
+
return message.content
|
|
76
|
+
.filter((block): block is TextContent => block.type === "text")
|
|
77
|
+
.map((block) => block.text)
|
|
78
|
+
.join("");
|
|
79
|
+
}
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Pull the user-visible text out of a streaming delta event, or null for any
|
|
85
|
+
* other event type. Used by the synchronous `onText` tap to forward
|
|
86
|
+
* `assistant_text_delta` / `assistant_thinking_delta` chunks to the caller.
|
|
87
|
+
*/
|
|
88
|
+
function extractDeltaText(msg: ServerMessage): string | null {
|
|
89
|
+
if (msg.type === "assistant_text_delta") return msg.text;
|
|
90
|
+
if (msg.type === "assistant_thinking_delta") return msg.thinking;
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
61
94
|
// ── Default subagent system prompt ──────────────────────────────────────
|
|
62
95
|
|
|
63
96
|
function buildSubagentSystemPrompt(
|
|
@@ -110,6 +143,19 @@ interface ManagedSubagent {
|
|
|
110
143
|
* the release to the TTL sweep rather than tearing down mid-drain.
|
|
111
144
|
*/
|
|
112
145
|
hadEnqueuedMessages?: boolean;
|
|
146
|
+
/**
|
|
147
|
+
* Set on the synchronous `spawnAndAwait` path. When true, `runSubagent`
|
|
148
|
+
* skips the terminal parent-injection (`notifyParentTerminal`) — the awaiting
|
|
149
|
+
* caller receives the child's final text directly, so re-injecting a
|
|
150
|
+
* "read the result" notification into the parent would be redundant noise.
|
|
151
|
+
*/
|
|
152
|
+
synchronous?: boolean;
|
|
153
|
+
/**
|
|
154
|
+
* Optional text tap for the synchronous path. When set, `wrappedSendToClient`
|
|
155
|
+
* forwards each `assistant_text_delta` / `assistant_thinking_delta` chunk to
|
|
156
|
+
* this callback IN ADDITION to the normal `subagent_event` envelope.
|
|
157
|
+
*/
|
|
158
|
+
onText?: (chunk: string) => void;
|
|
113
159
|
}
|
|
114
160
|
|
|
115
161
|
export interface SubagentNotificationInfo {
|
|
@@ -121,6 +167,21 @@ export interface SubagentNotificationInfo {
|
|
|
121
167
|
objective?: string;
|
|
122
168
|
}
|
|
123
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Thrown by `spawnAndAwait` when the run is aborted (e.g. an external timeout)
|
|
172
|
+
* before reaching a terminal `completed` state. Carries `partialText` — the
|
|
173
|
+
* child's trailing assistant text captured at the moment of abort — so a caller
|
|
174
|
+
* that times out a long generation can still surface the partial result instead
|
|
175
|
+
* of discarding it. Extends `Error` with the same legacy message, so callers
|
|
176
|
+
* that only inspect `.message` keep working.
|
|
177
|
+
*/
|
|
178
|
+
export class SubagentAbortedError extends Error {
|
|
179
|
+
constructor(readonly partialText: string) {
|
|
180
|
+
super("Subagent run aborted before completion.");
|
|
181
|
+
this.name = "SubagentAbortedError";
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
124
185
|
export class SubagentManager {
|
|
125
186
|
/** subagentId → ManagedSubagent */
|
|
126
187
|
private subagents = new Map<string, ManagedSubagent>();
|
|
@@ -145,6 +206,30 @@ export class SubagentManager {
|
|
|
145
206
|
config: Omit<SubagentConfig, "id">,
|
|
146
207
|
parentSendToClient: (msg: ServerMessage) => void,
|
|
147
208
|
): Promise<string> {
|
|
209
|
+
const { subagentId } = await this.setUpSubagent(config, parentSendToClient);
|
|
210
|
+
|
|
211
|
+
// ── Kick off the agent loop (fire-and-forget) ───────────────────
|
|
212
|
+
this.runSubagent(subagentId, config.objective).catch((err) => {
|
|
213
|
+
log.error({ subagentId, err }, "Subagent run failed unexpectedly");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return subagentId;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Internal: shared spawn setup ──────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Perform all spawn-time setup shared by `spawn` and `spawnAndAwait`:
|
|
223
|
+
* enforce the depth limit, resolve role/provider/system prompt, construct
|
|
224
|
+
* the child Conversation, register it, and emit the `subagent_spawned`
|
|
225
|
+
* event. Does NOT start the agent loop — the caller decides whether to run
|
|
226
|
+
* fire-and-forget (`spawn`) or awaited (`spawnAndAwait`).
|
|
227
|
+
*/
|
|
228
|
+
private async setUpSubagent(
|
|
229
|
+
config: Omit<SubagentConfig, "id">,
|
|
230
|
+
parentSendToClient: (msg: ServerMessage) => void,
|
|
231
|
+
opts?: { synchronous?: boolean; onText?: (chunk: string) => void },
|
|
232
|
+
): Promise<{ subagentId: string; managed: ManagedSubagent }> {
|
|
148
233
|
// ── Limit checks ────────────────────────────────────────────────
|
|
149
234
|
|
|
150
235
|
// Depth check: prevent subagents from spawning nested subagents.
|
|
@@ -159,17 +244,20 @@ export class SubagentManager {
|
|
|
159
244
|
|
|
160
245
|
// ── Resolve role ─────────────────────────────────────────────────
|
|
161
246
|
const isFork = config.fork === true;
|
|
162
|
-
|
|
247
|
+
const role: SubagentRole = (config.role as SubagentRole) ?? "general";
|
|
163
248
|
if (isFork && role !== "general") {
|
|
249
|
+
// A context-inheriting subagent normally keeps the parent's `general`
|
|
250
|
+
// role so its KV cache stays aligned with the parent conversation. An
|
|
251
|
+
// explicit non-general role opts out of that alignment on purpose
|
|
252
|
+
// (e.g. the advisor role running on a stronger profile), so honor it.
|
|
164
253
|
log.warn(
|
|
165
254
|
{
|
|
166
255
|
requestedRole: role,
|
|
167
256
|
parentConversationId: config.parentConversationId,
|
|
168
257
|
label: config.label,
|
|
169
258
|
},
|
|
170
|
-
"Fork requested with non-general role —
|
|
259
|
+
"Fork requested with non-general role — caller opted out of parent KV-cache alignment",
|
|
171
260
|
);
|
|
172
|
-
role = "general";
|
|
173
261
|
}
|
|
174
262
|
if (!SUBAGENT_ROLE_REGISTRY[role]) {
|
|
175
263
|
throw new Error(
|
|
@@ -219,19 +307,21 @@ export class SubagentManager {
|
|
|
219
307
|
|
|
220
308
|
let systemPrompt: string;
|
|
221
309
|
if (isFork) {
|
|
222
|
-
// Forks
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
310
|
+
// Forks default to the parent's system prompt verbatim — no subagent
|
|
311
|
+
// preamble — so the KV cache stays aligned with the parent. An explicit
|
|
312
|
+
// `systemPromptOverride` opts out of that alignment and takes precedence
|
|
313
|
+
// (e.g. the advisor role framing the inherited context as advice).
|
|
314
|
+
const resolved =
|
|
315
|
+
config.systemPromptOverride ??
|
|
316
|
+
config.parentSystemPrompt ??
|
|
317
|
+
parentConversation?.getCurrentSystemPrompt();
|
|
318
|
+
if (!resolved) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
"Fork spawn requires a parent system prompt but neither config.parentSystemPrompt " +
|
|
321
|
+
"nor findConversation yielded one.",
|
|
322
|
+
);
|
|
234
323
|
}
|
|
324
|
+
systemPrompt = resolved;
|
|
235
325
|
} else {
|
|
236
326
|
systemPrompt =
|
|
237
327
|
config.systemPromptOverride ??
|
|
@@ -279,11 +369,20 @@ export class SubagentManager {
|
|
|
279
369
|
conversation: null! as Conversation,
|
|
280
370
|
state,
|
|
281
371
|
parentSendToClient,
|
|
372
|
+
...(opts?.synchronous ? { synchronous: true } : {}),
|
|
373
|
+
...(opts?.onText ? { onText: opts.onText } : {}),
|
|
282
374
|
};
|
|
283
375
|
|
|
284
376
|
// Wrap sendToClient to envelope all events with the subagent ID.
|
|
285
377
|
// Reads from managed.parentSendToClient so reconnects are picked up.
|
|
286
378
|
const wrappedSendToClient = (msg: ServerMessage): void => {
|
|
379
|
+
// Tap streaming text/thinking deltas for the synchronous caller (if any),
|
|
380
|
+
// in addition to the normal envelope below. Reads from managed.onText so
|
|
381
|
+
// the synchronous path can forward chunks without altering event routing.
|
|
382
|
+
if (managed.onText) {
|
|
383
|
+
const text = extractDeltaText(msg);
|
|
384
|
+
if (text) managed.onText(text);
|
|
385
|
+
}
|
|
287
386
|
managed.parentSendToClient({
|
|
288
387
|
type: "subagent_event",
|
|
289
388
|
subagentId,
|
|
@@ -298,7 +397,16 @@ export class SubagentManager {
|
|
|
298
397
|
systemPrompt,
|
|
299
398
|
wrappedSendToClient,
|
|
300
399
|
workingDir,
|
|
301
|
-
{
|
|
400
|
+
{
|
|
401
|
+
maxTokens,
|
|
402
|
+
cacheTtl: "5m",
|
|
403
|
+
// The advisor consult runs tool-less for CLIENT tools but should ground
|
|
404
|
+
// its guidance with provider-native web search when the resolved
|
|
405
|
+
// provider supports it. This is a server tool the provider runs itself,
|
|
406
|
+
// so it stays one-shot — no client tool surfaced, allowlist unchanged.
|
|
407
|
+
// Other roles keep the default (no native search appended).
|
|
408
|
+
...(role === "advisor" ? { enableNativeWebSearch: true } : {}),
|
|
409
|
+
},
|
|
302
410
|
);
|
|
303
411
|
|
|
304
412
|
// Mark conversation as having no direct client — it routes through parent.
|
|
@@ -325,16 +433,19 @@ export class SubagentManager {
|
|
|
325
433
|
conversation.setAssistantId(parentConversation.assistantId);
|
|
326
434
|
}
|
|
327
435
|
|
|
328
|
-
if (isFork) {
|
|
329
|
-
//
|
|
330
|
-
//
|
|
436
|
+
if (isFork && !config.systemPromptOverride) {
|
|
437
|
+
// A verbatim-prompt fork pins the parent's system prompt as-is, skipping
|
|
438
|
+
// the dynamic rebuild so the KV cache stays aligned with the parent. A
|
|
439
|
+
// fork that supplies its own override prompt opts out of that alignment,
|
|
440
|
+
// so leave `hasSystemPromptOverride` at its default.
|
|
331
441
|
conversation.hasSystemPromptOverride = true;
|
|
332
442
|
}
|
|
333
443
|
|
|
334
|
-
// Apply role
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
|
|
444
|
+
// Apply the role's tool allowlist when one is defined. The `general` role
|
|
445
|
+
// has `allowedTools: undefined`, so default forks (which keep the general
|
|
446
|
+
// role) are unaffected; a fork carrying an explicit role gets its
|
|
447
|
+
// allowlist applied like any other subagent.
|
|
448
|
+
if (roleConfig.allowedTools) {
|
|
338
449
|
conversation.setSubagentAllowedTools(new Set(roleConfig.allowedTools));
|
|
339
450
|
}
|
|
340
451
|
|
|
@@ -393,12 +504,65 @@ export class SubagentManager {
|
|
|
393
504
|
"Subagent spawned",
|
|
394
505
|
);
|
|
395
506
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
log.error({ subagentId, err }, "Subagent run failed unexpectedly");
|
|
399
|
-
});
|
|
507
|
+
return { subagentId, managed };
|
|
508
|
+
}
|
|
400
509
|
|
|
401
|
-
|
|
510
|
+
// ── Spawn and await (synchronous) ─────────────────────────────────────
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Spawn a subagent and AWAIT its run, resolving to the child's final
|
|
514
|
+
* assistant text. Unlike `spawn` (fire-and-forget), the caller blocks until
|
|
515
|
+
* the child reaches a terminal state and receives the text directly — so the
|
|
516
|
+
* terminal parent-injection (`notifyParentTerminal`) is skipped on this path.
|
|
517
|
+
*
|
|
518
|
+
* `opts.signal` aborts the underlying run when triggered (e.g. an external
|
|
519
|
+
* timeout). `opts.onText` receives each streaming text/thinking chunk in
|
|
520
|
+
* addition to the normal `subagent_event` envelope.
|
|
521
|
+
*/
|
|
522
|
+
async spawnAndAwait(
|
|
523
|
+
config: Omit<SubagentConfig, "id">,
|
|
524
|
+
parentSendToClient: (msg: ServerMessage) => void,
|
|
525
|
+
opts?: { signal?: AbortSignal; onText?: (chunk: string) => void },
|
|
526
|
+
): Promise<string> {
|
|
527
|
+
const { subagentId, managed } = await this.setUpSubagent(
|
|
528
|
+
config,
|
|
529
|
+
parentSendToClient,
|
|
530
|
+
{ synchronous: true, ...(opts?.onText ? { onText: opts.onText } : {}) },
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
// Wire the external signal to abort the child conversation. If the signal
|
|
534
|
+
// is already aborted, abort immediately so the run rejects promptly.
|
|
535
|
+
const signal = opts?.signal;
|
|
536
|
+
const onAbort = (): void => {
|
|
537
|
+
// Route through the manager abort path so the subagent is marked terminal
|
|
538
|
+
// ("aborted") and broadcast as such. A bare conversation.abort() leaves
|
|
539
|
+
// status non-terminal, so runSubagent's success branch would record the
|
|
540
|
+
// run as "completed" once runAgentLoop resolves the consumed cancellation.
|
|
541
|
+
// Suppress the parent notification: the awaiting caller observes the abort
|
|
542
|
+
// as a thrown rejection, so a "do NOT re-spawn" injection would be
|
|
543
|
+
// redundant noise.
|
|
544
|
+
this.abort(subagentId, managed.parentSendToClient, undefined, {
|
|
545
|
+
suppressNotification: true,
|
|
546
|
+
});
|
|
547
|
+
};
|
|
548
|
+
if (signal) {
|
|
549
|
+
if (signal.aborted) onAbort();
|
|
550
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
try {
|
|
554
|
+
const finalText = await this.runSubagent(subagentId, config.objective);
|
|
555
|
+
// Surface aborts as a rejection so the caller's timeout path is
|
|
556
|
+
// observable — but carry the partial text on the error so a caller that
|
|
557
|
+
// timed out a long generation (e.g. the advisor consult) can still
|
|
558
|
+
// surface what was produced instead of throwing it away.
|
|
559
|
+
if (signal?.aborted) {
|
|
560
|
+
throw new SubagentAbortedError(finalText);
|
|
561
|
+
}
|
|
562
|
+
return finalText;
|
|
563
|
+
} finally {
|
|
564
|
+
signal?.removeEventListener("abort", onAbort);
|
|
565
|
+
}
|
|
402
566
|
}
|
|
403
567
|
|
|
404
568
|
// ── Internal: run the subagent ────────────────────────────────────────
|
|
@@ -406,14 +570,31 @@ export class SubagentManager {
|
|
|
406
570
|
private async runSubagent(
|
|
407
571
|
subagentId: string,
|
|
408
572
|
objective: string,
|
|
409
|
-
): Promise<
|
|
573
|
+
): Promise<string> {
|
|
410
574
|
const managed = this.subagents.get(subagentId);
|
|
411
|
-
if (!managed) return;
|
|
575
|
+
if (!managed) return "";
|
|
412
576
|
|
|
413
577
|
// Capture the live conversation — it is non-null at this point because
|
|
414
578
|
// spawn() sets it before firing runSubagent.
|
|
415
579
|
const conversation = managed.conversation!;
|
|
416
580
|
|
|
581
|
+
// The child's trailing assistant text, captured after runAgentLoop resolves
|
|
582
|
+
// (before the `finally` releases the conversation). Returned to the
|
|
583
|
+
// synchronous `spawnAndAwait` caller; the fire-and-forget `spawn` caller
|
|
584
|
+
// ignores it.
|
|
585
|
+
let finalText = "";
|
|
586
|
+
|
|
587
|
+
// Aborted before the run started (e.g. an already-aborted signal on the
|
|
588
|
+
// synchronous spawnAndAwait path): the subagent is already terminal. Do not
|
|
589
|
+
// start the agent loop or reset status back to "running" — but still release
|
|
590
|
+
// the conversation, exactly as the post-run `finally` does for a terminal
|
|
591
|
+
// run. The loop never started, so no messages were enqueued; this matches
|
|
592
|
+
// the finally's non-deferred release branch.
|
|
593
|
+
if (TERMINAL_STATUSES.has(managed.state.status)) {
|
|
594
|
+
this.releaseConversation(managed);
|
|
595
|
+
return finalText;
|
|
596
|
+
}
|
|
597
|
+
|
|
417
598
|
// Read the current parent sender so reconnects are picked up.
|
|
418
599
|
const getSender = () => managed.parentSendToClient;
|
|
419
600
|
|
|
@@ -441,7 +622,16 @@ export class SubagentManager {
|
|
|
441
622
|
// the fork tends to continue the parent conversation instead of
|
|
442
623
|
// pivoting to the task — the inherited context is louder than a bare
|
|
443
624
|
// objective buried after 100k+ tokens of chat history.
|
|
444
|
-
|
|
625
|
+
//
|
|
626
|
+
// The advisor consult is the exception: it is a fork, but its
|
|
627
|
+
// `systemPromptOverride` already frames the inherited context as advice
|
|
628
|
+
// ("you are a senior advisor … do not write its final deliverable"), so
|
|
629
|
+
// the generic "complete this task and return your findings" wrapper would
|
|
630
|
+
// fight that framing. The advisor's objective is already the bare advice
|
|
631
|
+
// request (`advisorRequestText()`), so it is sent uncontested.
|
|
632
|
+
const useForkFraming =
|
|
633
|
+
managed.state.isFork && managed.state.config.role !== "advisor";
|
|
634
|
+
const message = useForkFraming
|
|
445
635
|
? [
|
|
446
636
|
"⎯⎯⎯ FORK TASK ⎯⎯⎯",
|
|
447
637
|
"You have been forked from the parent conversation to execute a specific task.",
|
|
@@ -466,6 +656,9 @@ export class SubagentManager {
|
|
|
466
656
|
});
|
|
467
657
|
|
|
468
658
|
// Agent loop completed successfully.
|
|
659
|
+
// Capture the trailing assistant text before any release nulls the
|
|
660
|
+
// conversation reference. The fire-and-forget caller ignores the return.
|
|
661
|
+
finalText = extractFinalAssistantText(conversation.messages);
|
|
469
662
|
// Copy usage stats from the conversation before sending status (which includes usage).
|
|
470
663
|
managed.state.usage = { ...conversation.usageStats };
|
|
471
664
|
// Only update state + notify if still non-terminal (guards against abort race).
|
|
@@ -476,7 +669,12 @@ export class SubagentManager {
|
|
|
476
669
|
log.info({ subagentId }, "Subagent completed");
|
|
477
670
|
|
|
478
671
|
// Notify the parent conversation so the LLM can call subagent_read.
|
|
479
|
-
|
|
672
|
+
// Skipped on the synchronous path — the awaiting caller receives the
|
|
673
|
+
// final text directly, so re-injecting a "read the result" prompt
|
|
674
|
+
// would be redundant noise in the parent.
|
|
675
|
+
if (!managed.synchronous) {
|
|
676
|
+
this.notifyParentTerminal(managed, "completed");
|
|
677
|
+
}
|
|
480
678
|
}
|
|
481
679
|
} catch (err) {
|
|
482
680
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -489,10 +687,22 @@ export class SubagentManager {
|
|
|
489
687
|
// Only update status if not already terminal (e.g. aborted).
|
|
490
688
|
if (!TERMINAL_STATUSES.has(managed.state.status)) {
|
|
491
689
|
this.setStatus(subagentId, "failed", getSender(), errorMsg);
|
|
492
|
-
|
|
690
|
+
// Skip terminal parent-injection on the synchronous path — the failure
|
|
691
|
+
// surfaces to the awaiting caller as a rejected promise instead.
|
|
692
|
+
if (!managed.synchronous) {
|
|
693
|
+
this.notifyParentTerminal(managed, "failed");
|
|
694
|
+
}
|
|
493
695
|
}
|
|
494
696
|
|
|
495
697
|
log.error({ subagentId, err }, "Subagent failed");
|
|
698
|
+
|
|
699
|
+
// Surface the failure to the synchronous caller. The fire-and-forget
|
|
700
|
+
// path has no awaiter, so re-throwing there only feeds the `.catch()`
|
|
701
|
+
// logger in `spawn` — harmless but noisy — so it is confined to the
|
|
702
|
+
// synchronous path.
|
|
703
|
+
if (managed.synchronous) {
|
|
704
|
+
throw err;
|
|
705
|
+
}
|
|
496
706
|
} finally {
|
|
497
707
|
// Release the heavyweight Conversation — output is already persisted in DB.
|
|
498
708
|
// drainQueue is async: it awaits buildPassthroughBatch (which awaits
|
|
@@ -516,6 +726,8 @@ export class SubagentManager {
|
|
|
516
726
|
this.releaseConversation(managed);
|
|
517
727
|
}
|
|
518
728
|
}
|
|
729
|
+
|
|
730
|
+
return finalText;
|
|
519
731
|
}
|
|
520
732
|
|
|
521
733
|
// ── Abort ─────────────────────────────────────────────────────────────
|
package/src/subagent/types.ts
CHANGED
|
@@ -116,7 +116,8 @@ export type SubagentRole =
|
|
|
116
116
|
| "researcher"
|
|
117
117
|
| "coder"
|
|
118
118
|
| "planner"
|
|
119
|
-
| "investigator"
|
|
119
|
+
| "investigator"
|
|
120
|
+
| "advisor";
|
|
120
121
|
|
|
121
122
|
export interface SubagentRoleConfig {
|
|
122
123
|
/**
|
|
@@ -198,4 +199,10 @@ export const SUBAGENT_ROLE_REGISTRY: Record<SubagentRole, SubagentRoleConfig> =
|
|
|
198
199
|
"If you approach context limits, stop investigating and produce the report from what you have — a partial report delivered is worth more than a complete investigation lost.",
|
|
199
200
|
].join(" "),
|
|
200
201
|
},
|
|
202
|
+
advisor: {
|
|
203
|
+
allowedTools: [],
|
|
204
|
+
skillIds: [],
|
|
205
|
+
systemPromptPreamble:
|
|
206
|
+
"You are a read-only senior advisor consulted for a one-shot strategic review. Read the inherited conversation, then return focused, high-leverage guidance in a single response. You have no tools — you cannot search, read files, or run commands — so reason from the context you were given.",
|
|
207
|
+
},
|
|
201
208
|
};
|
package/src/tools/registry.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPluginDisabled } from "../plugins/disabled-state.js";
|
|
1
2
|
import { getLogger } from "../util/logger.js";
|
|
2
3
|
import { coreAppProxyTools } from "./apps/definitions.js";
|
|
3
4
|
import { registerAppTools } from "./apps/registry.js";
|
|
@@ -556,9 +557,15 @@ export function getMcpToolDefinitions(): Tool[] {
|
|
|
556
557
|
* {@link getMcpToolDefinitions} so a plugin install behaves like `mcp reload`.
|
|
557
558
|
*/
|
|
558
559
|
export function getPluginToolDefinitions(): Tool[] {
|
|
559
|
-
return Array.from(tools.values()).filter(
|
|
560
|
-
|
|
561
|
-
|
|
560
|
+
return Array.from(tools.values()).filter((t) => {
|
|
561
|
+
const owner = ownersByName.get(t.name);
|
|
562
|
+
if (owner?.kind !== "plugin") return false;
|
|
563
|
+
// Filter out tools contributed by disabled plugins at read time so
|
|
564
|
+
// `assistant plugins disable <name>` takes effect on the next turn
|
|
565
|
+
// without a daemon restart. Mirrors the `.disabled` sentinel filtering
|
|
566
|
+
// in `getHooksFor` (plugins/registry.ts).
|
|
567
|
+
return !isPluginDisabled(owner.id);
|
|
568
|
+
});
|
|
562
569
|
}
|
|
563
570
|
|
|
564
571
|
/**
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A progress-aware deadline for the synchronous advisor consult.
|
|
3
|
+
*
|
|
4
|
+
* A reasoning advisor profile spends most of its window *thinking*, streaming
|
|
5
|
+
* reasoning tokens the whole time. A fixed wall-clock ceiling would cut it off
|
|
6
|
+
* mid-thought, so this aborts only after the consult goes `idleMs` without any
|
|
7
|
+
* streamed token (thinking or text) — i.e. genuine silence — with an absolute
|
|
8
|
+
* `maxMs` backstop so a runaway or looping stream can't block the parent
|
|
9
|
+
* forever.
|
|
10
|
+
*
|
|
11
|
+
* Usage: combine `signal` with the caller's own signal, call `recordProgress()`
|
|
12
|
+
* on every streamed chunk, and `dispose()` once the consult settles.
|
|
13
|
+
*/
|
|
14
|
+
export interface ConsultDeadline {
|
|
15
|
+
/** Aborts when the idle window lapses or the absolute max elapses. */
|
|
16
|
+
readonly signal: AbortSignal;
|
|
17
|
+
/** Reset the idle window — call on every streamed chunk (thinking or text). */
|
|
18
|
+
recordProgress(): void;
|
|
19
|
+
/** Clear both timers; call once the consult settles (success or failure). */
|
|
20
|
+
dispose(): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createConsultDeadline(opts: {
|
|
24
|
+
idleMs: number;
|
|
25
|
+
maxMs: number;
|
|
26
|
+
}): ConsultDeadline {
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
let idleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
29
|
+
|
|
30
|
+
const recordProgress = (): void => {
|
|
31
|
+
// Once aborted, don't re-arm — the consult is already being torn down.
|
|
32
|
+
if (controller.signal.aborted) return;
|
|
33
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
34
|
+
idleTimer = setTimeout(() => controller.abort(), opts.idleMs);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Absolute backstop, independent of streaming progress.
|
|
38
|
+
const maxTimer = setTimeout(() => controller.abort(), opts.maxMs);
|
|
39
|
+
|
|
40
|
+
// Arm the idle window immediately so time-to-first-token is bounded too.
|
|
41
|
+
recordProgress();
|
|
42
|
+
|
|
43
|
+
const dispose = (): void => {
|
|
44
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
45
|
+
clearTimeout(maxTimer);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return { signal: controller.signal, recordProgress, dispose };
|
|
49
|
+
}
|