@vellumai/assistant 0.11.4-staging.2 → 0.11.4-staging.3
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/AGENTS.md +8 -2
- package/ARCHITECTURE.md +2 -0
- package/docs/architecture/memory.md +15 -0
- package/docs/browser-use-architecture-phase2.md +128 -56
- package/docs/flux-turn-detection-spike.md +11 -6
- package/knip.json +3 -0
- package/openapi.yaml +136 -76
- package/package.json +1 -1
- package/scripts/write-plugin-api-shim.ts +10 -0
- package/src/__tests__/app-control-flow.test.ts +1 -0
- package/src/__tests__/approval-routes-http.test.ts +2 -2
- package/src/__tests__/assistant-feature-flag-guard.test.ts +25 -3
- package/src/__tests__/channel-setup-panel-ack.test.ts +1 -1
- package/src/__tests__/compaction-events.test.ts +8 -10
- package/src/__tests__/conversation-agent-loop.test.ts +4 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +112 -0
- package/src/__tests__/conversation-notifiers-provenance.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +39 -62
- package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
- package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +9 -9
- package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
- package/src/__tests__/conversation-slash-queue.test.ts +3 -0
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-app-open.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-history-restored-completion.test.ts +21 -14
- package/src/__tests__/conversation-surfaces-queued-emit.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-standalone.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-state-update.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-table-action.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -1
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +1 -1
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +1 -1
- package/src/__tests__/cu-unified-flow.test.ts +1 -0
- package/src/__tests__/document-sync-tags.test.ts +0 -75
- package/src/__tests__/gateway-only-guard.test.ts +2 -5
- package/src/__tests__/http-user-message-parity.test.ts +1 -1
- package/src/__tests__/init-feature-flag-overrides.test.ts +49 -0
- package/src/__tests__/managed-skill-lifecycle.test.ts +7 -0
- package/src/__tests__/media-generate-image.test.ts +131 -21
- package/src/__tests__/memory-retrieval-hook.test.ts +94 -2
- package/src/__tests__/plugin-api-webhook-url.test.ts +10 -7
- package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +7 -3
- package/src/__tests__/proxy-approval-callback.test.ts +1 -0
- package/src/__tests__/qdrant-manager.test.ts +14 -1
- package/src/__tests__/run-due-schedules.test.ts +21 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +187 -18
- package/src/__tests__/schedule-routes.test.ts +23 -0
- package/src/__tests__/schedule-store.test.ts +17 -0
- package/src/__tests__/secret-ingress-http.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +3 -3
- package/src/__tests__/starter-task-flow.test.ts +5 -4
- package/src/__tests__/subagent-fork-prompt-role.test.ts +1 -1
- package/src/__tests__/subagent-spawn-and-await.test.ts +4 -7
- package/src/__tests__/subagent-tool-gate-mode.test.ts +1 -1
- package/src/__tests__/subagent-tools.test.ts +81 -101
- package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +1 -0
- package/src/__tests__/ui-choice-copy-surfaces.test.ts +1 -1
- package/src/__tests__/ui-visual-surface.test.ts +1 -1
- package/src/__tests__/ui-voice-picker-surface.test.ts +1 -1
- package/src/__tests__/ui-work-result-surface.test.ts +1 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +5 -3
- package/src/__tests__/voice-session-bridge.test.ts +85 -29
- package/src/acp/session-manager.ts +8 -1
- package/src/api/surfaces.ts +5 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +21 -10
- package/src/calls/__tests__/voice-triage-escalate.test.ts +8 -0
- package/src/calls/voice-session-bridge.ts +44 -25
- package/src/calls/voice-triage-escalate.ts +1 -0
- package/src/cli/bundled-modules.ts +29 -0
- package/src/cli/commands/db/repair.ts +4 -8
- package/src/cli/commands/domain.ts +6 -3
- package/src/cli/commands/email.ts +6 -3
- package/src/cli/commands/keys.ts +8 -3
- package/src/cli/commands/plugins.ts +85 -36
- package/src/cli/commands/schedules.ts +35 -1
- package/src/cli/lib/bundled-marketplace.json +1 -1
- package/src/config/__tests__/balanced-model-experiment.test.ts +278 -0
- package/src/config/assistant-feature-flags.ts +36 -15
- package/src/config/balanced-model-experiment.ts +35 -0
- package/src/config/bundled-skills/image-studio/SKILL.md +5 -4
- package/src/config/bundled-skills/image-studio/TOOLS.json +1 -1
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +101 -0
- package/src/config/bundled-skills/skill-management/TOOLS.json +9 -3
- package/src/config/bundled-skills/subagent/SKILL.md +17 -12
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -4
- package/src/config/call-site-defaults.ts +7 -0
- package/src/config/default-profile-catalog.ts +96 -4
- package/src/config/feature-flag-registry.json +11 -11
- package/src/config/skills.ts +9 -2
- package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +1 -1
- package/src/daemon/conversation-agent-loop.ts +14 -13
- package/src/daemon/conversation-notifiers.ts +11 -9
- package/src/daemon/conversation-process.ts +0 -27
- package/src/daemon/conversation-store.ts +4 -4
- package/src/daemon/conversation-surfaces.ts +27 -13
- package/src/daemon/conversation-tool-setup.ts +2 -4
- package/src/daemon/conversation.ts +81 -44
- package/src/daemon/doordash-steps.ts +2 -2
- package/src/daemon/lifecycle.ts +14 -1
- package/src/daemon/process-message.ts +0 -13
- package/src/daemon/windows-compiled-entry.ts +4 -0
- package/src/documents/document-store.ts +5 -235
- package/src/hooks/types.ts +5 -0
- package/src/ipc/gateway-flag-listener.ts +17 -3
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +118 -0
- package/src/live-voice/live-voice-manager.ts +16 -3
- package/src/live-voice/live-voice-session.ts +63 -9
- package/src/live-voice/windows-compiled-live-voice.ts +4 -0
- package/src/monitoring/control.ts +1 -0
- package/src/monitoring/db-integrity-sample.ts +4 -5
- package/src/permissions/prompter.ts +1 -5
- package/src/persistence/conversation-queries.ts +66 -16
- package/src/persistence/embeddings/qdrant-manager.ts +84 -49
- package/src/persistence/migrations/360-add-document-workspace-path.ts +5 -14
- package/src/persistence/schema/documents.ts +4 -4
- package/src/plugin-api/constants.ts +8 -0
- package/src/plugin-api/index.ts +5 -1
- package/src/plugin-api/webhook-url.ts +13 -11
- package/src/plugins/defaults/main.ts +6 -7
- package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +77 -0
- package/src/plugins/defaults/memory/graph/conversation-graph-memory.ts +24 -6
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +53 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +4 -4
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +18 -0
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +66 -1
- package/src/plugins/defaults/memory/v3/injector.ts +8 -0
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +23 -9
- package/src/plugins/defaults/memory/worker-control.ts +1 -0
- package/src/plugins/defaults/worker-entrypoints.ts +3 -0
- package/src/plugins/mtime-cache.ts +17 -0
- package/src/prompts/templates/system-sections.ts +0 -7
- package/src/providers/__tests__/context-overflow-error.test.ts +24 -0
- package/src/providers/__tests__/retry-callsite.test.ts +20 -0
- package/src/providers/openai/chat-completions-provider.ts +11 -1
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +11 -6
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +9 -49
- package/src/routes/control.ts +1 -0
- package/src/routes/route-host-client.ts +1 -0
- package/src/runtime/AGENTS.md +16 -17
- package/src/runtime/agent-wake.ts +15 -12
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +170 -1
- package/src/runtime/routes/__tests__/schedule-routes-disarm-reason.test.ts +215 -0
- package/src/runtime/routes/conversation-list-routes.ts +54 -22
- package/src/runtime/routes/conversation-management-routes.ts +2 -3
- package/src/runtime/routes/conversation-routes.ts +11 -13
- package/src/runtime/routes/documents-routes.ts +3 -222
- package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +2 -0
- package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +3 -0
- package/src/runtime/routes/playground/inject-failures.ts +2 -2
- package/src/runtime/routes/playground/reset-circuit.ts +1 -1
- package/src/runtime/routes/schedule-routes.ts +94 -7
- package/src/runtime/routes/workspace-routes.ts +0 -9
- package/src/runtime/routes/workspace-utils.ts +3 -13
- package/src/runtime/services/conversation-serializer.ts +7 -2
- package/src/schedule/__tests__/plugin-schedule-declarations.test.ts +68 -5
- package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +81 -0
- package/src/schedule/plugin-schedule-availability.ts +58 -0
- package/src/schedule/plugin-schedule-declarations.ts +23 -27
- package/src/schedule/plugin-schedule-reconciler.ts +12 -3
- package/src/schedule/schedule-store.ts +5 -1
- package/src/schedule/scheduler.ts +9 -4
- package/src/schedule/worker-control.ts +1 -0
- package/src/subagent/__tests__/consult-prompt.test.ts +26 -15
- package/src/subagent/consult-context.ts +11 -11
- package/src/subagent/consult-prompt.ts +26 -35
- package/src/subagent/manager.ts +20 -37
- package/src/subagent/notify.ts +7 -1
- package/src/subagent/types.ts +8 -6
- package/src/tools/acp/spawn.ts +6 -4
- package/src/tools/skills/scaffold-managed.ts +25 -7
- package/src/tools/subagent/spawn.ts +28 -88
- package/src/tools/ui-surface/surface-shape-docs.ts +1 -1
- package/src/util/__tests__/worker-process-command.test.ts +37 -0
- package/src/util/logger.ts +16 -0
- package/src/util/worker-process.ts +37 -4
- package/src/windows-compiled-cli.ts +32 -0
- package/src/windows-compiled-entry.ts +4 -0
- package/src/windows-compiled-logger.ts +6 -0
- package/src/windows-compiled-worker-entry.ts +29 -0
- package/src/__tests__/document-workspace-file.test.ts +0 -467
- package/src/daemon/interactive-turn-sender.ts +0 -59
- package/src/subagent/__tests__/consult-transcript.test.ts +0 -184
- package/src/subagent/consult-transcript.ts +0 -90
|
@@ -40,6 +40,18 @@ mock.module("../daemon/conversation-store.js", () => ({
|
|
|
40
40
|
},
|
|
41
41
|
}));
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Whether the daemon activated the fixture plugin. Run-now answers only for
|
|
45
|
+
* plugins this process brought up, and no route test runs the plugin loader,
|
|
46
|
+
* so the double says "activated" unless a case turns it off.
|
|
47
|
+
*/
|
|
48
|
+
let pluginActivated = true;
|
|
49
|
+
const realMtimeCache = await import("../plugins/mtime-cache.js");
|
|
50
|
+
mock.module("../plugins/mtime-cache.js", () => ({
|
|
51
|
+
...realMtimeCache,
|
|
52
|
+
isPluginDirActivated: () => pluginActivated,
|
|
53
|
+
}));
|
|
54
|
+
|
|
43
55
|
import type { AssistantEventEnvelope } from "../api/index.js";
|
|
44
56
|
import { SYNC_TAGS } from "../daemon/message-types/sync.js";
|
|
45
57
|
import {
|
|
@@ -1574,6 +1586,7 @@ describe("plugin-sourced schedules over routes", () => {
|
|
|
1574
1586
|
|
|
1575
1587
|
beforeEach(() => {
|
|
1576
1588
|
rmSync(RUN_MARKER, { force: true });
|
|
1589
|
+
pluginActivated = true;
|
|
1577
1590
|
});
|
|
1578
1591
|
|
|
1579
1592
|
function seedSourcedScript() {
|
|
@@ -1626,6 +1639,16 @@ describe("plugin-sourced schedules over routes", () => {
|
|
|
1626
1639
|
expect(existsSync(RUN_MARKER)).toBe(false);
|
|
1627
1640
|
});
|
|
1628
1641
|
|
|
1642
|
+
test("refuses a sourced row whose plugin the daemon never activated", async () => {
|
|
1643
|
+
const sourced = await seedSourcedScript();
|
|
1644
|
+
// The declaration is intact on disk, but nothing ran the plugin's
|
|
1645
|
+
// `init`, so run-now must not execute its script.
|
|
1646
|
+
pluginActivated = false;
|
|
1647
|
+
|
|
1648
|
+
await expect(runNow(sourced.id)).rejects.toThrow(BadRequestError);
|
|
1649
|
+
expect(existsSync(RUN_MARKER)).toBe(false);
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1629
1652
|
test("runs a sourced row whose plugin is healthy", async () => {
|
|
1630
1653
|
const sourced = await seedSourcedScript();
|
|
1631
1654
|
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from "../persistence/conversation-crud.js";
|
|
24
24
|
import { getDb } from "../persistence/db-connection.js";
|
|
25
25
|
import { initializeDb } from "../persistence/db-init.js";
|
|
26
|
+
import { isPluginDirActivated } from "../plugins/mtime-cache.js";
|
|
26
27
|
import { assistantEventHub } from "../runtime/assistant-event-hub.js";
|
|
27
28
|
import {
|
|
28
29
|
hasOwnerDeferProvenance,
|
|
@@ -1856,6 +1857,22 @@ describe("declared schedules", () => {
|
|
|
1856
1857
|
expect(result!.enabled).toBe(false);
|
|
1857
1858
|
});
|
|
1858
1859
|
|
|
1860
|
+
test("enabling a schedule re-arms it on the on-disk declaration alone", async () => {
|
|
1861
|
+
const created = await upsertDeclaredSchedule(SOURCE_KEY, makeDefinition());
|
|
1862
|
+
await setUserEnabled(created.id, false);
|
|
1863
|
+
// A schedule tool reaches this from a conversation turn, which can run in
|
|
1864
|
+
// a sidecar worker process where no plugin is activated. The probe must
|
|
1865
|
+
// therefore read the disk alone: asking about activation here would refuse
|
|
1866
|
+
// every re-enable. Whether the plugin stays armed is the reconciler's
|
|
1867
|
+
// call, and its next sweep disarms the row if it should not be.
|
|
1868
|
+
expect(isPluginDirActivated(examplePluginDir())).toBe(false);
|
|
1869
|
+
|
|
1870
|
+
const result = await setUserEnabled(created.id, true);
|
|
1871
|
+
|
|
1872
|
+
expect(result!.userEnabled).toBe(true);
|
|
1873
|
+
expect(result!.enabled).toBe(true);
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1859
1876
|
test("enabling a schedule whose plugin manifest is broken records the override without re-arming", async () => {
|
|
1860
1877
|
const created = await upsertDeclaredSchedule(SOURCE_KEY, makeDefinition());
|
|
1861
1878
|
await setUserEnabled(created.id, false);
|
|
@@ -155,7 +155,7 @@ const runAgentLoopMock = mock(async () => undefined);
|
|
|
155
155
|
function makeSendMessageDeps() {
|
|
156
156
|
const session = {
|
|
157
157
|
setTrustContext: () => {},
|
|
158
|
-
|
|
158
|
+
replayActivityState: () => {},
|
|
159
159
|
emitConfirmationStateChanged: () => {},
|
|
160
160
|
emitActivityState: () => {},
|
|
161
161
|
setTurnChannelContext: () => {},
|
|
@@ -130,7 +130,7 @@ function makeCompletingConversation(): Conversation {
|
|
|
130
130
|
setTurnInterfaceContext: () => {},
|
|
131
131
|
ensureActorScopedHistory: async () => {},
|
|
132
132
|
usageStats: { inputTokens: 0, outputTokens: 0, estimatedCost: 0 },
|
|
133
|
-
|
|
133
|
+
replayActivityState: () => {},
|
|
134
134
|
setHostBrowserProxy: () => {},
|
|
135
135
|
setHostCuProxy: () => {},
|
|
136
136
|
setHostAppControlProxy: () => {},
|
|
@@ -180,7 +180,7 @@ function makeHangingConversation(): Conversation {
|
|
|
180
180
|
setTurnInterfaceContext: () => {},
|
|
181
181
|
ensureActorScopedHistory: async () => {},
|
|
182
182
|
usageStats: { inputTokens: 0, outputTokens: 0, estimatedCost: 0 },
|
|
183
|
-
|
|
183
|
+
replayActivityState: () => {},
|
|
184
184
|
setHostBrowserProxy: () => {},
|
|
185
185
|
setHostCuProxy: () => {},
|
|
186
186
|
setHostAppControlProxy: () => {},
|
|
@@ -261,7 +261,7 @@ function makePendingApprovalConversation(
|
|
|
261
261
|
setTurnInterfaceContext: () => {},
|
|
262
262
|
ensureActorScopedHistory: async () => {},
|
|
263
263
|
usageStats: { inputTokens: 0, outputTokens: 0, estimatedCost: 0 },
|
|
264
|
-
|
|
264
|
+
replayActivityState: () => {},
|
|
265
265
|
setHostBrowserProxy: () => {},
|
|
266
266
|
setHostCuProxy: () => {},
|
|
267
267
|
setHostAppControlProxy: () => {},
|
|
@@ -39,10 +39,12 @@ import {
|
|
|
39
39
|
} from "../daemon/conversation-surfaces.js";
|
|
40
40
|
import { asConversation } from "./helpers/mock-conversation.js";
|
|
41
41
|
|
|
42
|
-
function makeContext(
|
|
42
|
+
function makeContext(
|
|
43
|
+
emit: (msg: AssistantEvent) => void = () => {},
|
|
44
|
+
): Conversation {
|
|
43
45
|
return asConversation({
|
|
44
46
|
conversationId: "session-1",
|
|
45
|
-
|
|
47
|
+
emit,
|
|
46
48
|
pendingSurfaceActions: new Map<string, { surfaceType: SurfaceType }>(),
|
|
47
49
|
lastSurfaceAction: new Map<
|
|
48
50
|
string,
|
|
@@ -131,8 +133,7 @@ describe("starter task surface actions", () => {
|
|
|
131
133
|
|
|
132
134
|
test("app_open registers dynamic_page surface as action-capable", async () => {
|
|
133
135
|
const sent: AssistantEvent[] = [];
|
|
134
|
-
const ctx = makeContext();
|
|
135
|
-
ctx.sendToClient = (msg) => sent.push(msg);
|
|
136
|
+
const ctx = makeContext((msg) => sent.push(msg));
|
|
136
137
|
|
|
137
138
|
const result = await surfaceProxyResolver(ctx, "app_open", {
|
|
138
139
|
app_id: "test-app",
|
|
@@ -182,7 +182,7 @@ describe("SubagentManager fork — prompt source and role decoupling", () => {
|
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
test("fork with systemPromptOverride uses that prompt and does not set hasSystemPromptOverride", async () => {
|
|
185
|
-
const overridePrompt = "You are
|
|
185
|
+
const overridePrompt = "You are a reviewer. Frame the context as review.";
|
|
186
186
|
const created = await spawnFork({ systemPromptOverride: overridePrompt });
|
|
187
187
|
|
|
188
188
|
expect(created.systemPrompt).toBe(overridePrompt);
|
|
@@ -589,20 +589,17 @@ describe("SubagentManager — first user message framing", () => {
|
|
|
589
589
|
await manager.spawnAndAwait(
|
|
590
590
|
makeConfig({
|
|
591
591
|
objective: "Please advise.",
|
|
592
|
-
fork: true,
|
|
593
592
|
role: "advisor",
|
|
593
|
+
spawnMode: "advisor_consult",
|
|
594
594
|
// The advisor always supplies its own framing; setUpSubagent uses it
|
|
595
|
-
//
|
|
595
|
+
// in place of a built subagent preamble.
|
|
596
596
|
systemPromptOverride: "You are a senior advisor.",
|
|
597
|
-
parentMessages: [
|
|
598
|
-
{ role: "user", content: [{ type: "text", text: "prior turn" }] },
|
|
599
|
-
],
|
|
600
597
|
}),
|
|
601
598
|
() => {},
|
|
602
599
|
);
|
|
603
600
|
|
|
604
|
-
// The
|
|
605
|
-
// directive would fight the advisor system prompt.
|
|
601
|
+
// The consult is a regular blocking spawn, so its user turn is the brief
|
|
602
|
+
// itself: the fork directive would fight the advisor system prompt.
|
|
606
603
|
expect(lastPersistedUserMessage).toBe("Please advise.");
|
|
607
604
|
expect(lastPersistedUserMessage).not.toContain("FORK TASK");
|
|
608
605
|
});
|
|
@@ -118,7 +118,7 @@ function makeSetupCtx(overrides: Partial<Conversation> = {}): Conversation {
|
|
|
118
118
|
workingDir: "/tmp/test",
|
|
119
119
|
getTurnActorPrincipalId: () => undefined,
|
|
120
120
|
abortController: null,
|
|
121
|
-
|
|
121
|
+
emit: mock(() => {}),
|
|
122
122
|
pendingSurfaceActions: new Map(),
|
|
123
123
|
lastSurfaceAction: new Map(),
|
|
124
124
|
surfaceState: new Map(),
|
|
@@ -1073,16 +1073,24 @@ describe("Subagent spawn repeat-loop guard", () => {
|
|
|
1073
1073
|
const objective = "Audit the advisor pipeline for drift";
|
|
1074
1074
|
seedSpawns("guard-advisor", objective, 6);
|
|
1075
1075
|
|
|
1076
|
-
const
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1076
|
+
const manager = getSubagentManager();
|
|
1077
|
+
const originalAwait = manager.spawnAndAwait.bind(manager);
|
|
1078
|
+
manager.spawnAndAwait = (async () =>
|
|
1079
|
+
"guidance") as unknown as typeof manager.spawnAndAwait;
|
|
1080
|
+
try {
|
|
1081
|
+
const { result } = await spawnWithGuard({
|
|
1082
|
+
label: "Consult",
|
|
1083
|
+
objective,
|
|
1084
|
+
role: "advisor",
|
|
1085
|
+
});
|
|
1081
1086
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1087
|
+
// The advisor branch runs and returns its guidance, so the guard never
|
|
1088
|
+
// saw the repetition.
|
|
1089
|
+
expect(result.content).toBe("guidance");
|
|
1090
|
+
expect(result.content).not.toContain("confirm_repeat");
|
|
1091
|
+
} finally {
|
|
1092
|
+
manager.spawnAndAwait = originalAwait;
|
|
1093
|
+
}
|
|
1086
1094
|
});
|
|
1087
1095
|
|
|
1088
1096
|
test("copies still running trip the guard before any of them completes", async () => {
|
|
@@ -2831,19 +2839,27 @@ describe("Subagent output contract", () => {
|
|
|
2831
2839
|
});
|
|
2832
2840
|
|
|
2833
2841
|
test("a null contract reads as omitted and reaches the advisor branch", async () => {
|
|
2834
|
-
const
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2842
|
+
const manager = getSubagentManager();
|
|
2843
|
+
const originalAwait = manager.spawnAndAwait.bind(manager);
|
|
2844
|
+
manager.spawnAndAwait = (async () =>
|
|
2845
|
+
"guidance") as unknown as typeof manager.spawnAndAwait;
|
|
2846
|
+
try {
|
|
2847
|
+
const result = await executeSubagentSpawn(
|
|
2848
|
+
{
|
|
2849
|
+
label: "Consult",
|
|
2850
|
+
objective: "Check the plan",
|
|
2851
|
+
role: "advisor",
|
|
2852
|
+
output_contract: null,
|
|
2853
|
+
},
|
|
2854
|
+
makeContext("sess-contract-advisor-none", { sendToClient: () => {} }),
|
|
2855
|
+
);
|
|
2856
|
+
// The advisor branch runs and returns its guidance, so the contract check
|
|
2857
|
+
// waved this spawn through.
|
|
2858
|
+
expect(result.content).toBe("guidance");
|
|
2859
|
+
expect(result.content).not.toContain("does not apply to the advisor");
|
|
2860
|
+
} finally {
|
|
2861
|
+
manager.spawnAndAwait = originalAwait;
|
|
2862
|
+
}
|
|
2847
2863
|
});
|
|
2848
2864
|
|
|
2849
2865
|
test("verdict defaults the child to the cost-optimized tier", async () => {
|
|
@@ -2998,7 +3014,6 @@ describe("Subagent output contract", () => {
|
|
|
2998
3014
|
// ── Advisor-role consult ────────────────────────────────────────────
|
|
2999
3015
|
|
|
3000
3016
|
describe("Subagent advisor-role consult", () => {
|
|
3001
|
-
type Block = { type: string; [k: string]: unknown };
|
|
3002
3017
|
type CapturedAwait = {
|
|
3003
3018
|
config: Record<string, unknown>;
|
|
3004
3019
|
opts?: {
|
|
@@ -3062,16 +3077,11 @@ describe("Subagent advisor-role consult", () => {
|
|
|
3062
3077
|
expect(result.content).toBe("Here is my advice.");
|
|
3063
3078
|
// Ran synchronously through spawnAndAwait, not fire-and-forget spawn.
|
|
3064
3079
|
expect(captured.current).toBeDefined();
|
|
3065
|
-
expect(captured.current!.config.fork).toBe(true);
|
|
3066
3080
|
expect(captured.current!.config.role).toBe("advisor");
|
|
3067
3081
|
// The advisor is a ROLE, not an `LLMCallSiteEnum` value, so its usage
|
|
3068
3082
|
// lands under `subagentSpawn` like any other subagent. The declared
|
|
3069
|
-
// spawn mode is what separates it from a plain
|
|
3083
|
+
// spawn mode is what separates it from a plain spawn in cost telemetry.
|
|
3070
3084
|
expect(captured.current!.config.spawnMode).toBe("advisor_consult");
|
|
3071
|
-
// Framing embeds the executor prompt as advisor system prompt context.
|
|
3072
|
-
expect(captured.current!.config.systemPromptOverride).toContain(
|
|
3073
|
-
"PARENT SYSTEM PROMPT",
|
|
3074
|
-
);
|
|
3075
3085
|
// The situational context pack must never ride display surfaces: the
|
|
3076
3086
|
// system prompt stays minimal and `objective` (rendered verbatim by the
|
|
3077
3087
|
// subagent detail panel) stays the concise advice request. Only the
|
|
@@ -3089,26 +3099,18 @@ describe("Subagent advisor-role consult", () => {
|
|
|
3089
3099
|
}
|
|
3090
3100
|
});
|
|
3091
3101
|
|
|
3092
|
-
test("advisor
|
|
3093
|
-
//
|
|
3094
|
-
//
|
|
3102
|
+
test("advisor carries no parent transcript or system prompt", async () => {
|
|
3103
|
+
// The consult is a regular blocking spawn: the brief is its whole input, so
|
|
3104
|
+
// nothing of the parent conversation is snapshotted into it.
|
|
3095
3105
|
mockFindConversation = () => ({
|
|
3096
3106
|
messages: [
|
|
3097
3107
|
{ role: "user", content: [{ type: "text", text: "Do the task" }] },
|
|
3098
3108
|
{
|
|
3099
3109
|
role: "assistant",
|
|
3100
|
-
content: [
|
|
3101
|
-
{ type: "thinking", thinking: "secret reasoning" },
|
|
3102
|
-
{ type: "text", text: "Working on it" },
|
|
3103
|
-
{ type: "tool_use", id: "t1", name: "bash", input: {} },
|
|
3104
|
-
],
|
|
3105
|
-
},
|
|
3106
|
-
{
|
|
3107
|
-
role: "user",
|
|
3108
|
-
content: [{ type: "tool_result", tool_use_id: "t1", content: "ok" }],
|
|
3110
|
+
content: [{ type: "text", text: "Working on it" }],
|
|
3109
3111
|
},
|
|
3110
3112
|
],
|
|
3111
|
-
getCurrentSystemPrompt: () => "
|
|
3113
|
+
getCurrentSystemPrompt: () => "PARENT SYSTEM PROMPT",
|
|
3112
3114
|
});
|
|
3113
3115
|
const { captured, restore } = stubAwait("advice");
|
|
3114
3116
|
try {
|
|
@@ -3116,77 +3118,55 @@ describe("Subagent advisor-role consult", () => {
|
|
|
3116
3118
|
{ label: "Consult", objective: "x", role: "advisor" },
|
|
3117
3119
|
makeContext("advisor-sess-2", { sendToClient: () => {} }),
|
|
3118
3120
|
);
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
expect(allBlocks.some((b) => b.type === "thinking")).toBe(false);
|
|
3126
|
-
expect(allBlocks.some((b) => b.type === "tool_use")).toBe(true);
|
|
3127
|
-
expect(allBlocks.some((b) => b.type === "tool_result")).toBe(true);
|
|
3121
|
+
expect(captured.current!.config.fork).toBeUndefined();
|
|
3122
|
+
expect(captured.current!.config.parentMessages).toBeUndefined();
|
|
3123
|
+
expect(captured.current!.config.parentSystemPrompt).toBeUndefined();
|
|
3124
|
+
expect(captured.current!.config.systemPromptOverride).not.toContain(
|
|
3125
|
+
"PARENT SYSTEM PROMPT",
|
|
3126
|
+
);
|
|
3128
3127
|
} finally {
|
|
3129
3128
|
restore();
|
|
3130
3129
|
mockFindConversation = () => undefined;
|
|
3131
3130
|
}
|
|
3132
3131
|
});
|
|
3133
3132
|
|
|
3134
|
-
test("
|
|
3135
|
-
// The
|
|
3136
|
-
//
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
});
|
|
3144
|
-
mockGetMessages = (convId: string) => {
|
|
3145
|
-
if (convId !== "advisor-sess-3") {
|
|
3146
|
-
return null;
|
|
3147
|
-
}
|
|
3148
|
-
return [
|
|
3149
|
-
{
|
|
3150
|
-
role: "user",
|
|
3151
|
-
content: [{ type: "text", text: "Plan the work" }],
|
|
3152
|
-
},
|
|
3153
|
-
{
|
|
3154
|
-
role: "assistant",
|
|
3155
|
-
content: [
|
|
3156
|
-
{ type: "text", text: "My plan: step 1, step 2." },
|
|
3157
|
-
{
|
|
3158
|
-
type: "tool_use",
|
|
3159
|
-
id: "adv-1",
|
|
3160
|
-
name: "subagent_spawn",
|
|
3161
|
-
input: {},
|
|
3162
|
-
},
|
|
3163
|
-
],
|
|
3164
|
-
},
|
|
3165
|
-
];
|
|
3166
|
-
};
|
|
3133
|
+
test("the agent's objective reaches the advisor as its brief", async () => {
|
|
3134
|
+
// The brief is the advisor's only account of the work, so it must travel
|
|
3135
|
+
// verbatim inside the request fence.
|
|
3136
|
+
const brief = [
|
|
3137
|
+
"Task: migrate the queue to the new store.",
|
|
3138
|
+
"Plan: dual-write, then cut over.",
|
|
3139
|
+
"Evidence: src/queue/store.ts already exports both writers.",
|
|
3140
|
+
"Question: is dual-write the right shape here?",
|
|
3141
|
+
].join("\n");
|
|
3167
3142
|
const { captured, restore } = stubAwait("advice");
|
|
3168
3143
|
try {
|
|
3169
3144
|
await executeSubagentSpawn(
|
|
3170
|
-
{ label: "Consult", objective:
|
|
3171
|
-
makeContext("advisor-sess-
|
|
3145
|
+
{ label: "Consult", objective: brief, role: "advisor" },
|
|
3146
|
+
makeContext("advisor-sess-brief", { sendToClient: () => {} }),
|
|
3147
|
+
);
|
|
3148
|
+
expect(captured.current!.config.requestText).toContain(
|
|
3149
|
+
`<agent_request>\n${brief}\n</agent_request>`,
|
|
3172
3150
|
);
|
|
3173
|
-
const msgs = captured.current!.config.parentMessages as Array<{
|
|
3174
|
-
role: string;
|
|
3175
|
-
content: Block[];
|
|
3176
|
-
}>;
|
|
3177
|
-
const allBlocks = msgs.flatMap((m) => m.content);
|
|
3178
|
-
// The plan text the model wrote this turn is present in the consult.
|
|
3179
|
-
expect(
|
|
3180
|
-
allBlocks.some(
|
|
3181
|
-
(b) => b.type === "text" && b.text === "My plan: step 1, step 2.",
|
|
3182
|
-
),
|
|
3183
|
-
).toBe(true);
|
|
3184
|
-
// No dangling tool_use is sent.
|
|
3185
|
-
expect(allBlocks.some((b) => b.type === "tool_use")).toBe(false);
|
|
3186
3151
|
} finally {
|
|
3187
3152
|
restore();
|
|
3188
|
-
|
|
3189
|
-
|
|
3153
|
+
}
|
|
3154
|
+
});
|
|
3155
|
+
|
|
3156
|
+
test("advisor consults still run when the parent conversation is gone", async () => {
|
|
3157
|
+
// The parent is looked up only for its warm skill catalog, so an evicted
|
|
3158
|
+
// conversation costs a section of the context pack, not the consult.
|
|
3159
|
+
mockFindConversation = () => undefined;
|
|
3160
|
+
const { captured, restore } = stubAwait("advice");
|
|
3161
|
+
try {
|
|
3162
|
+
const result = await executeSubagentSpawn(
|
|
3163
|
+
{ label: "Consult", objective: "advise me", role: "advisor" },
|
|
3164
|
+
makeContext("advisor-sess-no-parent", { sendToClient: () => {} }),
|
|
3165
|
+
);
|
|
3166
|
+
expect(result.content).toBe("advice");
|
|
3167
|
+
expect(captured.current!.config.role).toBe("advisor");
|
|
3168
|
+
} finally {
|
|
3169
|
+
restore();
|
|
3190
3170
|
}
|
|
3191
3171
|
});
|
|
3192
3172
|
|
|
@@ -104,6 +104,7 @@ function makeContext(): Conversation {
|
|
|
104
104
|
conversationId: CONVERSATION_ID,
|
|
105
105
|
trustContext: { trustClass: "guardian", sourceChannel: "vellum" },
|
|
106
106
|
sendToClient: () => {},
|
|
107
|
+
emit: () => {},
|
|
107
108
|
pendingSurfaceActions: new Map<string, { surfaceType: SurfaceType }>(),
|
|
108
109
|
lastSurfaceAction: new Map(),
|
|
109
110
|
surfaceState: new Map(),
|
|
@@ -22,7 +22,7 @@ import { asConversation } from "./helpers/mock-conversation.js";
|
|
|
22
22
|
function makeContext(sent: AssistantEvent[] = []): Conversation {
|
|
23
23
|
return asConversation({
|
|
24
24
|
conversationId: "session-1",
|
|
25
|
-
|
|
25
|
+
emit: (msg) => sent.push(msg),
|
|
26
26
|
pendingSurfaceActions: new Map<string, { surfaceType: SurfaceType }>(),
|
|
27
27
|
lastSurfaceAction: new Map<
|
|
28
28
|
string,
|
|
@@ -32,7 +32,7 @@ const HTML = '<div style="color:var(--content-default)">Hello</div>';
|
|
|
32
32
|
function makeContext(sent: AssistantEvent[] = []): Conversation {
|
|
33
33
|
return asConversation({
|
|
34
34
|
conversationId: "session-1",
|
|
35
|
-
|
|
35
|
+
emit: (msg) => sent.push(msg),
|
|
36
36
|
pendingSurfaceActions: new Map<string, { surfaceType: SurfaceType }>(),
|
|
37
37
|
lastSurfaceAction: new Map<
|
|
38
38
|
string,
|
|
@@ -26,7 +26,7 @@ import { asConversation } from "./helpers/mock-conversation.js";
|
|
|
26
26
|
function makeContext(sent: AssistantEvent[] = []): Conversation {
|
|
27
27
|
return asConversation({
|
|
28
28
|
conversationId: "session-1",
|
|
29
|
-
|
|
29
|
+
emit: (msg) => sent.push(msg),
|
|
30
30
|
pendingSurfaceActions: new Map<string, { surfaceType: SurfaceType }>(),
|
|
31
31
|
lastSurfaceAction: new Map<
|
|
32
32
|
string,
|
|
@@ -19,7 +19,7 @@ import { asConversation } from "./helpers/mock-conversation.js";
|
|
|
19
19
|
function makeContext(sent: AssistantEvent[] = []): Conversation {
|
|
20
20
|
return asConversation({
|
|
21
21
|
conversationId: "session-1",
|
|
22
|
-
|
|
22
|
+
emit: (msg) => sent.push(msg),
|
|
23
23
|
pendingSurfaceActions: new Map<string, { surfaceType: SurfaceType }>(),
|
|
24
24
|
lastSurfaceAction: new Map<
|
|
25
25
|
string,
|
|
@@ -89,7 +89,8 @@ const CALL_SESSION_ID = "call-session-voice-grant-test";
|
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
91
|
* Create a mock session that, when runAgentLoop is called, emits a
|
|
92
|
-
* confirmation_request through the
|
|
92
|
+
* confirmation_request through the registered event observer before
|
|
93
|
+
* completing.
|
|
93
94
|
*/
|
|
94
95
|
function createMockSession(opts?: {
|
|
95
96
|
confirmationRequestId?: string;
|
|
@@ -118,8 +119,9 @@ function createMockSession(opts?: {
|
|
|
118
119
|
currentRequestId: requestId,
|
|
119
120
|
abort: () => {},
|
|
120
121
|
persistUserMessage: async () => ({ id: "msg-1", deduplicated: false }),
|
|
121
|
-
|
|
122
|
+
addEventObserver: (cb: (msg: AssistantEvent) => void) => {
|
|
122
123
|
clientCallback = cb;
|
|
124
|
+
return () => {};
|
|
123
125
|
},
|
|
124
126
|
handleConfirmationResponse: (
|
|
125
127
|
reqId: string,
|
|
@@ -138,7 +140,7 @@ function createMockSession(opts?: {
|
|
|
138
140
|
_messageId: string,
|
|
139
141
|
options?: { onEvent?: (msg: AssistantEvent) => void },
|
|
140
142
|
) => {
|
|
141
|
-
// Emit a confirmation_request through the
|
|
143
|
+
// Emit a confirmation_request through the event observer
|
|
142
144
|
if (clientCallback) {
|
|
143
145
|
clientCallback({
|
|
144
146
|
type: "confirmation_request",
|