@vellumai/assistant 0.12.2-staging.7 → 0.12.2
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/docs/architecture/memory.md +2 -11
- package/docs/desktop-browser-cli.md +2 -4
- package/node_modules/@vellumai/environments/src/shell.test.ts +0 -21
- package/node_modules/@vellumai/environments/src/shell.ts +0 -24
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +2 -8
- package/openapi.yaml +2 -6
- package/package.json +1 -1
- package/scripts/smoke-desktop-browser-cli.ts +0 -1
- package/src/__tests__/agent-loop.test.ts +0 -124
- package/src/__tests__/approval-interception-trust-gates.test.ts +0 -40
- package/src/__tests__/channel-approval.test.ts +14 -9
- package/src/__tests__/conversation-agent-loop.test.ts +0 -25
- package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +1 -138
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/subagent-tool-gate-mode.test.ts +0 -169
- package/src/__tests__/terminal-tools.test.ts +0 -8
- package/src/__tests__/unicode.test.ts +0 -36
- package/src/agent/loop.ts +0 -19
- package/src/api/index.ts +0 -6
- package/src/approvals/approval-primitive.ts +2 -5
- package/src/approvals/scoped-approval-grants.ts +2 -6
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -43
- package/src/daemon/conversation-agent-loop.ts +0 -2
- package/src/daemon/conversation-tool-setup.ts +1 -57
- package/src/daemon/conversation.ts +0 -17
- package/src/daemon/daemon-control.ts +6 -2
- package/src/daemon/orphan-reaper.ts +3 -4
- package/src/daemon/tool-setup-types.ts +0 -6
- package/src/daemon/wake-conversation-ops.ts +15 -38
- package/src/desktop/desktop-automation-lease.test.ts +0 -143
- package/src/desktop/desktop-automation-lease.ts +3 -39
- package/src/messaging/provider-message-metadata.ts +3 -3
- package/src/notifications/__tests__/copy-composer.test.ts +0 -70
- package/src/notifications/copy-composer.ts +3 -11
- package/src/oauth/seed-providers.ts +30 -0
- package/src/persistence/conversation-plugin-facade.ts +0 -13
- package/src/persistence/schema/index.ts +0 -1
- package/src/persistence/steps.ts +0 -2
- package/src/plugin-api/conversation-turn.ts +7 -31
- package/src/plugin-api/index.ts +1 -9
- package/src/plugins/defaults/memory/AGENTS.md +2 -14
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +0 -43
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +0 -46
- package/src/plugins/defaults/memory/buffer-format.ts +0 -40
- package/src/plugins/defaults/memory/context-search/agent-runner.ts +2 -1
- package/src/plugins/defaults/memory/context-search/format.ts +1 -2
- package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +1 -2
- package/src/plugins/defaults/memory/context-search/sources/workspace.ts +1 -2
- package/src/plugins/defaults/memory/graph/capability-seed.ts +2 -1
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +42 -1
- package/src/plugins/defaults/memory/host-utils.ts +10 -0
- package/src/plugins/defaults/memory/injectors.ts +3 -4
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +181 -55
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +99 -407
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +0 -10
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +7 -107
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +86 -307
- package/src/plugins/defaults/memory/substrate/page-index.ts +1 -2
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +49 -89
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +1 -1
- package/src/plugins/defaults/memory/tools.ts +1 -1
- package/src/plugins/defaults/memory/v1/graph/consolidation.ts +2 -2
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +1 -2
- package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
- package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +0 -5
- package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +2 -5
- package/src/plugins/defaults/memory/v2/reranker.ts +1 -2
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +1 -81
- package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +0 -87
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +0 -21
- package/src/plugins/defaults/memory/v3/card.ts +1 -2
- package/src/plugins/defaults/memory/v3/injector.ts +178 -212
- package/src/plugins/defaults/memory/v3/orchestrate.ts +22 -86
- package/src/plugins/defaults/memory/v3/pool-select.ts +7 -10
- package/src/plugins/defaults/memory/v3/sections.ts +1 -2
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +1 -10
- package/src/plugins/defaults/tool-result-truncate/terminal.ts +46 -1
- package/src/runtime/AGENTS.md +1 -1
- package/src/runtime/__tests__/agent-wake.test.ts +1 -86
- package/src/runtime/agent-wake.ts +4 -20
- package/src/runtime/guardian-action-service.ts +17 -2
- package/src/runtime/guardian-reply-router.ts +8 -1
- package/src/runtime/routes/channel-route-shared.ts +9 -1
- package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
- package/src/runtime/routes/desktop-setup-routes.ts +3 -7
- package/src/runtime/routes/guardian-approval-interception.ts +0 -24
- package/src/runtime/routes/inbound-message-handler.ts +3 -2
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -7
- package/src/schedule/run-script.ts +2 -2
- package/src/tools/host-terminal/host-shell.ts +6 -12
- package/src/tools/shared/filesystem/file-ops-service.ts +31 -1
- package/src/tools/shared/shell-output.test.ts +0 -10
- package/src/tools/shared/shell-output.ts +2 -14
- package/src/tools/skills/sandbox-runner.ts +2 -13
- package/src/tools/skills/scaffold-managed.ts +1 -2
- package/src/tools/terminal/__tests__/safe-env.test.ts +0 -29
- package/src/tools/terminal/safe-env.ts +1 -30
- package/src/tools/terminal/sanitized-bash.ts +2 -15
- package/src/tools/terminal/shell.test.ts +0 -29
- package/src/tools/terminal/shell.ts +7 -13
- package/src/util/host-process.test.ts +1 -17
- package/src/util/host-process.ts +0 -24
- package/src/util/unicode.ts +0 -29
- package/src/__tests__/db-conversation-tool-surface.test.ts +0 -144
- package/src/api/events/desktop-activity-changed.ts +0 -10
- package/src/persistence/conversation-tool-surface.ts +0 -86
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +0 -78
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +0 -29
- package/src/persistence/schema/conversation-tool-surfaces.ts +0 -22
- package/src/plugin-api/plugin-channel-turn-trust.test.ts +0 -133
- package/src/plugin-api/plugin-channel-turn-trust.ts +0 -71
- package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +0 -320
- package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +0 -17
- package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +0 -161
- package/src/plugins/defaults/memory/buffer-file.ts +0 -354
- package/src/plugins/defaults/memory/memory-run-evidence.ts +0 -213
- package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +0 -34
- package/src/tools/terminal/shell-launch.test.ts +0 -162
|
@@ -66,34 +66,6 @@ mock.module("../daemon/conversation-skill-tools.js", () => ({
|
|
|
66
66
|
})),
|
|
67
67
|
}));
|
|
68
68
|
|
|
69
|
-
// Records every wire tool surface the resolver persists (conversation id +
|
|
70
|
-
// tool names + the hash it already knew), and lets a test make the write fail.
|
|
71
|
-
let recordedSurfaces: Array<{
|
|
72
|
-
conversationId: string;
|
|
73
|
-
toolNames: string[];
|
|
74
|
-
knownHash: string | undefined;
|
|
75
|
-
}> = [];
|
|
76
|
-
let recordSurfaceThrows: Error | null = null;
|
|
77
|
-
mock.module("../persistence/conversation-tool-surface.js", () => ({
|
|
78
|
-
recordConversationToolSurface: (
|
|
79
|
-
conversationId: string,
|
|
80
|
-
tools: ToolDefinition[],
|
|
81
|
-
knownHash?: string,
|
|
82
|
-
) => {
|
|
83
|
-
recordedSurfaces.push({
|
|
84
|
-
conversationId,
|
|
85
|
-
toolNames: tools.map((t) => t.name),
|
|
86
|
-
knownHash,
|
|
87
|
-
});
|
|
88
|
-
if (recordSurfaceThrows) {
|
|
89
|
-
throw recordSurfaceThrows;
|
|
90
|
-
}
|
|
91
|
-
return `hash:${tools.map((t) => t.name).join(",")}`;
|
|
92
|
-
},
|
|
93
|
-
hashConversationToolSurface: (tools: ToolDefinition[]) =>
|
|
94
|
-
`hash:${tools.map((t) => t.name).join(",")}`,
|
|
95
|
-
}));
|
|
96
|
-
|
|
97
69
|
// ---------------------------------------------------------------------------
|
|
98
70
|
// Imports after mocks are in place
|
|
99
71
|
// ---------------------------------------------------------------------------
|
|
@@ -103,7 +75,6 @@ import { createSurfaceMutex } from "../daemon/conversation-surfaces.js";
|
|
|
103
75
|
import {
|
|
104
76
|
createResolveToolsCallback,
|
|
105
77
|
createToolExecutor,
|
|
106
|
-
createWireToolSurfaceRecorder,
|
|
107
78
|
isRefusedInReadOnlyPass,
|
|
108
79
|
type SubagentToolStats,
|
|
109
80
|
} from "../daemon/conversation-tool-setup.js";
|
|
@@ -1146,143 +1117,3 @@ describe("createResolveToolsCallback — read-only hides dynamic MCP tools", ()
|
|
|
1146
1117
|
expect(resolve(EMPTY_HISTORY).map((t) => t.name)).toContain("srv_send");
|
|
1147
1118
|
});
|
|
1148
1119
|
});
|
|
1149
|
-
|
|
1150
|
-
// ---------------------------------------------------------------------------
|
|
1151
|
-
// Resolver: wire tool surface record + replay (fork cache parity)
|
|
1152
|
-
// ---------------------------------------------------------------------------
|
|
1153
|
-
|
|
1154
|
-
describe("createResolveToolsCallback: wire tool surface record and replay", () => {
|
|
1155
|
-
beforeEach(() => {
|
|
1156
|
-
recordedSurfaces = [];
|
|
1157
|
-
recordSurfaceThrows = null;
|
|
1158
|
-
projectedSkillToolNames = [];
|
|
1159
|
-
});
|
|
1160
|
-
|
|
1161
|
-
test("the resolver itself never records: out-of-band callers read it too", () => {
|
|
1162
|
-
const toolDefs = [makeToolDef("remember"), makeToolDef("tool_b")];
|
|
1163
|
-
const ctx = makeProjectionCtx({ conversationId: "conv-live" });
|
|
1164
|
-
const resolve = createResolveToolsCallback(toolDefs, ctx)!;
|
|
1165
|
-
|
|
1166
|
-
// The `/compact` token count resolves outside any turn, where presence
|
|
1167
|
-
// reads clientless; recording here would overwrite the sent surface.
|
|
1168
|
-
resolve(EMPTY_HISTORY);
|
|
1169
|
-
|
|
1170
|
-
expect(recordedSurfaces).toEqual([]);
|
|
1171
|
-
expect(ctx.recordedToolSurfaceHash).toBeUndefined();
|
|
1172
|
-
});
|
|
1173
|
-
|
|
1174
|
-
test("the send-boundary recorder records the sent array, keyed by conversation, and remembers its hash", () => {
|
|
1175
|
-
const ctx = makeProjectionCtx({ conversationId: "conv-live" });
|
|
1176
|
-
const record = createWireToolSurfaceRecorder(ctx);
|
|
1177
|
-
const sent = [makeToolDef("remember"), makeToolDef("web_search")];
|
|
1178
|
-
|
|
1179
|
-
record(sent);
|
|
1180
|
-
record(sent);
|
|
1181
|
-
|
|
1182
|
-
// Every provider call records; the persistence layer dedupes on the hash
|
|
1183
|
-
// handed back from the previous call.
|
|
1184
|
-
expect(recordedSurfaces).toEqual([
|
|
1185
|
-
{
|
|
1186
|
-
conversationId: "conv-live",
|
|
1187
|
-
toolNames: ["remember", "web_search"],
|
|
1188
|
-
knownHash: undefined,
|
|
1189
|
-
},
|
|
1190
|
-
{
|
|
1191
|
-
conversationId: "conv-live",
|
|
1192
|
-
toolNames: ["remember", "web_search"],
|
|
1193
|
-
knownHash: "hash:remember,web_search",
|
|
1194
|
-
},
|
|
1195
|
-
]);
|
|
1196
|
-
expect(ctx.recordedToolSurfaceHash).toBe("hash:remember,web_search");
|
|
1197
|
-
});
|
|
1198
|
-
|
|
1199
|
-
test("a failed record is swallowed and still counts as recorded", () => {
|
|
1200
|
-
recordSurfaceThrows = new Error("no such table");
|
|
1201
|
-
const record = createWireToolSurfaceRecorder(
|
|
1202
|
-
makeProjectionCtx({ conversationId: "conv-live" }),
|
|
1203
|
-
);
|
|
1204
|
-
|
|
1205
|
-
expect(() => record([makeToolDef("remember")])).not.toThrow();
|
|
1206
|
-
record([makeToolDef("remember")]);
|
|
1207
|
-
|
|
1208
|
-
// The second call carries the hash from the failed first one, so a
|
|
1209
|
-
// persistent failure is retried once per distinct surface, not per call.
|
|
1210
|
-
expect(recordedSurfaces.map((r) => r.knownHash)).toEqual([
|
|
1211
|
-
undefined,
|
|
1212
|
-
"hash:remember",
|
|
1213
|
-
]);
|
|
1214
|
-
});
|
|
1215
|
-
|
|
1216
|
-
test("the recorder skips arrays that are not the conversation's own surface", () => {
|
|
1217
|
-
const tools = [makeToolDef("remember")];
|
|
1218
|
-
|
|
1219
|
-
// A replaying wake sends its source's array.
|
|
1220
|
-
createWireToolSurfaceRecorder(
|
|
1221
|
-
makeProjectionCtx({ conversationId: "conv-fork", wireToolReplay: tools }),
|
|
1222
|
-
)(tools);
|
|
1223
|
-
// A tools-disabled call sends nothing; a fork replaying [] could never
|
|
1224
|
-
// call remember.
|
|
1225
|
-
createWireToolSurfaceRecorder(
|
|
1226
|
-
makeProjectionCtx({ conversationId: "conv-live" }),
|
|
1227
|
-
)([]);
|
|
1228
|
-
// Disk-pressure cleanup narrows the wire to cleanup tools.
|
|
1229
|
-
createWireToolSurfaceRecorder(
|
|
1230
|
-
makeProjectionCtx({
|
|
1231
|
-
conversationId: "conv-live",
|
|
1232
|
-
diskPressureCleanupModeActive: true,
|
|
1233
|
-
}),
|
|
1234
|
-
)(tools);
|
|
1235
|
-
|
|
1236
|
-
expect(recordedSurfaces).toEqual([]);
|
|
1237
|
-
});
|
|
1238
|
-
|
|
1239
|
-
test("replays wireToolReplay verbatim on the wire while execution still derives from the fork's own context", () => {
|
|
1240
|
-
projectedSkillToolNames = ["scaffold_managed_skill"];
|
|
1241
|
-
const replay: ToolDefinition[] = [
|
|
1242
|
-
makeToolDef("bash"),
|
|
1243
|
-
makeToolDef("remember"),
|
|
1244
|
-
makeToolDef("bell_jingle"),
|
|
1245
|
-
{
|
|
1246
|
-
type: "web_search_20250305",
|
|
1247
|
-
name: "web_search",
|
|
1248
|
-
max_uses: 5,
|
|
1249
|
-
} as unknown as ToolDefinition,
|
|
1250
|
-
];
|
|
1251
|
-
// The fork's own registry lacks bell_jingle (a user-plugin tool the
|
|
1252
|
-
// worker never loads) and web_search (a server tool the loop appends).
|
|
1253
|
-
const toolDefs = [makeToolDef("remember"), makeToolDef("bash")];
|
|
1254
|
-
const ctx = makeProjectionCtx({
|
|
1255
|
-
conversationId: "conv-fork",
|
|
1256
|
-
subagentAllowedTools: new Set(["remember"]),
|
|
1257
|
-
subagentToolGateMode: "execution",
|
|
1258
|
-
wireToolReplay: replay,
|
|
1259
|
-
});
|
|
1260
|
-
const resolve = createResolveToolsCallback(toolDefs, ctx)!;
|
|
1261
|
-
|
|
1262
|
-
const tools = resolve(EMPTY_HISTORY);
|
|
1263
|
-
|
|
1264
|
-
// Byte-identical to the source's recorded array, in the source's order,
|
|
1265
|
-
// including definitions this process could never resolve.
|
|
1266
|
-
expect(JSON.stringify(tools)).toBe(JSON.stringify(replay));
|
|
1267
|
-
// Execution-side inventory is still the fork's own derivation (plus the
|
|
1268
|
-
// preactivated skill tools), never widened by the replayed definitions.
|
|
1269
|
-
expect(ctx.allowedToolNames).toEqual(
|
|
1270
|
-
new Set(["remember", "bash", "scaffold_managed_skill"]),
|
|
1271
|
-
);
|
|
1272
|
-
});
|
|
1273
|
-
|
|
1274
|
-
test("replay returns a fresh array each call so the loop cannot mutate the recorded one", () => {
|
|
1275
|
-
const replay = [makeToolDef("remember")];
|
|
1276
|
-
const ctx = makeProjectionCtx({
|
|
1277
|
-
conversationId: "conv-fork",
|
|
1278
|
-
wireToolReplay: replay,
|
|
1279
|
-
});
|
|
1280
|
-
const resolve = createResolveToolsCallback([makeToolDef("remember")], ctx)!;
|
|
1281
|
-
|
|
1282
|
-
const first = resolve(EMPTY_HISTORY);
|
|
1283
|
-
first.push(makeToolDef("injected"));
|
|
1284
|
-
|
|
1285
|
-
expect(resolve(EMPTY_HISTORY).map((t) => t.name)).toEqual(["remember"]);
|
|
1286
|
-
expect(replay.map((t) => t.name)).toEqual(["remember"]);
|
|
1287
|
-
});
|
|
1288
|
-
});
|
|
@@ -44,7 +44,6 @@ import {
|
|
|
44
44
|
formatShellOutput,
|
|
45
45
|
MAX_OUTPUT_LENGTH,
|
|
46
46
|
OUTPUT_TRUNCATED_TAG,
|
|
47
|
-
SHELL_DID_NOT_START_MESSAGE,
|
|
48
47
|
} from "../tools/shared/shell-output.js";
|
|
49
48
|
import {
|
|
50
49
|
ALWAYS_INJECTED_ENV_VARS,
|
|
@@ -335,13 +334,6 @@ describe("formatShellOutput", () => {
|
|
|
335
334
|
expect(result.isError).toBe(false);
|
|
336
335
|
});
|
|
337
336
|
|
|
338
|
-
test("a close with exit 0 and empty pipes is an error when the process never started", () => {
|
|
339
|
-
const result = formatShellOutput("", "", 0, false, 120, { started: false });
|
|
340
|
-
expect(result.content).toBe(SHELL_DID_NOT_START_MESSAGE);
|
|
341
|
-
expect(result.isError).toBe(true);
|
|
342
|
-
expect(result.content).not.toBe("<command_completed />");
|
|
343
|
-
});
|
|
344
|
-
|
|
345
337
|
test("failed command with no output shows exit code tag and descriptive message", () => {
|
|
346
338
|
const result = formatShellOutput("", "", 1, false, 120);
|
|
347
339
|
expect(result.content).toContain('<command_exit code="1" />');
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
safeStringSlice,
|
|
5
5
|
stripOrphanedSurrogates,
|
|
6
6
|
stripOrphanedSurrogatesDeep,
|
|
7
|
-
surrogateSafeWindow,
|
|
8
7
|
} from "../util/unicode.js";
|
|
9
8
|
|
|
10
9
|
// U+1F389 PARTY POPPER = "\uD83C\uDF89" (a surrogate pair).
|
|
@@ -303,38 +302,3 @@ describe("stripOrphanedSurrogatesDeep", () => {
|
|
|
303
302
|
expect(() => JSON.parse(json)).not.toThrow();
|
|
304
303
|
});
|
|
305
304
|
});
|
|
306
|
-
|
|
307
|
-
describe("surrogateSafeWindow", () => {
|
|
308
|
-
const codeAt = (text: string) => (i: number) => text.charCodeAt(i);
|
|
309
|
-
|
|
310
|
-
test("a start that lands on a low surrogate backs up to include the pair", () => {
|
|
311
|
-
const text = `a${EMOJI}b`;
|
|
312
|
-
expect(surrogateSafeWindow(text.length, codeAt(text), 2, 10)).toEqual({
|
|
313
|
-
start: 1,
|
|
314
|
-
end: 4,
|
|
315
|
-
});
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
test("an end that would cut a pair backs off one unit", () => {
|
|
319
|
-
const text = `ab${EMOJI}cd`;
|
|
320
|
-
expect(surrogateSafeWindow(text.length, codeAt(text), 0, 3)).toEqual({
|
|
321
|
-
start: 0,
|
|
322
|
-
end: 2,
|
|
323
|
-
});
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
test("a one-unit window on a pair takes the whole pair so paging advances", () => {
|
|
327
|
-
const text = `${EMOJI}x`;
|
|
328
|
-
expect(surrogateSafeWindow(text.length, codeAt(text), 0, 1)).toEqual({
|
|
329
|
-
start: 0,
|
|
330
|
-
end: 2,
|
|
331
|
-
});
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
test("clamps to the text bounds", () => {
|
|
335
|
-
expect(surrogateSafeWindow(3, codeAt("abc"), 10, 5)).toEqual({
|
|
336
|
-
start: 3,
|
|
337
|
-
end: 3,
|
|
338
|
-
});
|
|
339
|
-
});
|
|
340
|
-
});
|
package/src/agent/loop.ts
CHANGED
|
@@ -1047,17 +1047,6 @@ export interface AgentLoopConstructorOptions {
|
|
|
1047
1047
|
tools?: ToolDefinition[];
|
|
1048
1048
|
toolExecutor?: LoopToolExecutor;
|
|
1049
1049
|
resolveTools?: (history: Message[]) => ToolDefinition[];
|
|
1050
|
-
/**
|
|
1051
|
-
* Observer for the final tool array of each provider call, invoked
|
|
1052
|
-
* immediately before the request leaves with exactly what goes on the wire
|
|
1053
|
-
* (after any provider-native tool is appended, past the inter-call throttle
|
|
1054
|
-
* and the pre-model hooks, and not at all once the run is aborted). This is
|
|
1055
|
-
* the only point that sees the sent array: the dynamic `resolveTools`
|
|
1056
|
-
* callback is also consulted out of band (token counting, compaction
|
|
1057
|
-
* estimates), so a consumer that needs "what the last request sent"
|
|
1058
|
-
* subscribes here rather than wrapping the resolver. Must not throw.
|
|
1059
|
-
*/
|
|
1060
|
-
onToolsSent?: (tools: ToolDefinition[]) => void;
|
|
1061
1050
|
/**
|
|
1062
1051
|
* Conversation this loop drives. Scopes the loop-held compaction circuit
|
|
1063
1052
|
* breaker and is the source of truth the loop's pipeline contexts and
|
|
@@ -1098,7 +1087,6 @@ export class AgentLoop {
|
|
|
1098
1087
|
private config: AgentLoopConfig;
|
|
1099
1088
|
private tools: ToolDefinition[];
|
|
1100
1089
|
private resolveTools: ((history: Message[]) => ToolDefinition[]) | null;
|
|
1101
|
-
private onToolsSent: ((tools: ToolDefinition[]) => void) | null;
|
|
1102
1090
|
private toolExecutor: LoopToolExecutor | null;
|
|
1103
1091
|
|
|
1104
1092
|
/**
|
|
@@ -1134,7 +1122,6 @@ export class AgentLoop {
|
|
|
1134
1122
|
tools,
|
|
1135
1123
|
toolExecutor,
|
|
1136
1124
|
resolveTools,
|
|
1137
|
-
onToolsSent,
|
|
1138
1125
|
conversationId,
|
|
1139
1126
|
resolveConversationDir,
|
|
1140
1127
|
transformCompactedHistory,
|
|
@@ -1144,7 +1131,6 @@ export class AgentLoop {
|
|
|
1144
1131
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
1145
1132
|
this.tools = tools ?? [];
|
|
1146
1133
|
this.resolveTools = resolveTools ?? null;
|
|
1147
|
-
this.onToolsSent = onToolsSent ?? null;
|
|
1148
1134
|
this.toolExecutor = toolExecutor ?? null;
|
|
1149
1135
|
this.conversationId = conversationId;
|
|
1150
1136
|
this.resolveConversationDir = resolveConversationDir ?? null;
|
|
@@ -2255,11 +2241,6 @@ export class AgentLoop {
|
|
|
2255
2241
|
// Latency: the request is about to leave for the provider. The span
|
|
2256
2242
|
// from here to the first streamed token is time-to-first-token.
|
|
2257
2243
|
latencyTracker?.mark("request_sent");
|
|
2258
|
-
// Every await that could cancel the call is behind us; a run aborted
|
|
2259
|
-
// during the throttle or a hook never reports an array it did not send.
|
|
2260
|
-
if (!signal?.aborted) {
|
|
2261
|
-
this.onToolsSent?.(currentTools);
|
|
2262
|
-
}
|
|
2263
2244
|
let response: ProviderResponse;
|
|
2264
2245
|
try {
|
|
2265
2246
|
response = await traceAsyncSection("agent-loop:provider-send", () =>
|
package/src/api/index.ts
CHANGED
|
@@ -34,7 +34,6 @@ import { ConversationErrorEventSchema } from "./events/conversation-error.js";
|
|
|
34
34
|
import { ConversationInferenceProfileUpdatedEventSchema } from "./events/conversation-inference-profile-updated.js";
|
|
35
35
|
import { ConversationNoticeEventSchema } from "./events/conversation-notice.js";
|
|
36
36
|
import { ConversationTitleUpdatedEventSchema } from "./events/conversation-title-updated.js";
|
|
37
|
-
import { DesktopActivityChangedEventSchema } from "./events/desktop-activity-changed.js";
|
|
38
37
|
import { DiskPressureStatusChangedEventSchema } from "./events/disk-pressure-status-changed.js";
|
|
39
38
|
import { DocumentCommentCreatedEventSchema } from "./events/document-comment-created.js";
|
|
40
39
|
import { DocumentCommentDeletedEventSchema } from "./events/document-comment-deleted.js";
|
|
@@ -343,10 +342,6 @@ export {
|
|
|
343
342
|
type ConversationTitleUpdatedEvent,
|
|
344
343
|
ConversationTitleUpdatedEventSchema,
|
|
345
344
|
} from "./events/conversation-title-updated.js";
|
|
346
|
-
export {
|
|
347
|
-
type DesktopActivityChangedEvent,
|
|
348
|
-
DesktopActivityChangedEventSchema,
|
|
349
|
-
} from "./events/desktop-activity-changed.js";
|
|
350
345
|
export {
|
|
351
346
|
type DiskPressureBlockedCapability,
|
|
352
347
|
DiskPressureBlockedCapabilitySchema,
|
|
@@ -1005,7 +1000,6 @@ export const AssistantEventSchema = z.discriminatedUnion("type", [
|
|
|
1005
1000
|
ConversationInferenceProfileUpdatedEventSchema,
|
|
1006
1001
|
ConversationNoticeEventSchema,
|
|
1007
1002
|
ConversationTitleUpdatedEventSchema,
|
|
1008
|
-
DesktopActivityChangedEventSchema,
|
|
1009
1003
|
DiskPressureStatusChangedEventSchema,
|
|
1010
1004
|
DocumentCommentCreatedEventSchema,
|
|
1011
1005
|
DocumentCommentDeletedEventSchema,
|
|
@@ -213,10 +213,7 @@ function consumeGrantSync(params: ConsumeGrantParams): ConsumeGrantResult {
|
|
|
213
213
|
return { ok: true, grant: reqResult.grant };
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
|
|
217
|
-
// poll this function, so a miss is the expected steady state while a
|
|
218
|
-
// guardian decides. Hits and the wait's outcome are logged at info.
|
|
219
|
-
log.debug(
|
|
216
|
+
log.info(
|
|
220
217
|
{
|
|
221
218
|
event: "approval_primitive_consume_miss",
|
|
222
219
|
mode: "request_id",
|
|
@@ -258,7 +255,7 @@ function consumeGrantSync(params: ConsumeGrantParams): ConsumeGrantResult {
|
|
|
258
255
|
return { ok: true, grant: sigResult.grant };
|
|
259
256
|
}
|
|
260
257
|
|
|
261
|
-
log.
|
|
258
|
+
log.info(
|
|
262
259
|
{
|
|
263
260
|
event: "approval_primitive_consume_miss",
|
|
264
261
|
mode: "tool_signature",
|
|
@@ -193,10 +193,7 @@ function consumeScopedApprovalGrantByRequestId(
|
|
|
193
193
|
.get();
|
|
194
194
|
|
|
195
195
|
if (!candidate) {
|
|
196
|
-
|
|
197
|
-
// this every poll, so a miss is the expected steady state while a
|
|
198
|
-
// guardian decides. The caller logs the outcome of the wait.
|
|
199
|
-
log.debug(
|
|
196
|
+
log.info(
|
|
200
197
|
{
|
|
201
198
|
event: "scoped_grant_consume_miss",
|
|
202
199
|
requestId,
|
|
@@ -367,8 +364,7 @@ function consumeScopedApprovalGrantByToolSignature(
|
|
|
367
364
|
.get();
|
|
368
365
|
|
|
369
366
|
if (!candidate) {
|
|
370
|
-
|
|
371
|
-
log.debug(
|
|
367
|
+
log.info(
|
|
372
368
|
{
|
|
373
369
|
event: "scoped_grant_consume_miss",
|
|
374
370
|
toolName: params.toolName,
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
|
|
7
7
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
8
8
|
|
|
9
|
-
import { CONSOLIDATION_ALLOWED_TOOLS } from "../../plugins/defaults/memory/substrate/consolidation-tool-surface.js";
|
|
10
9
|
import type { Conversation } from "../conversation.js";
|
|
11
10
|
import type { ChannelCapabilities } from "../conversation-runtime-assembly.js";
|
|
12
11
|
|
|
@@ -780,45 +779,3 @@ describe("isToolActiveForContext — allowlist-only tools (delete_memory_page)",
|
|
|
780
779
|
).toBe(false);
|
|
781
780
|
});
|
|
782
781
|
});
|
|
783
|
-
|
|
784
|
-
describe("isToolActiveForContext — memory consolidation surface resolves onto the wire", () => {
|
|
785
|
-
// A background job conversation is not a subagent, so SUBAGENT_ONLY tools
|
|
786
|
-
// are filtered off before the allowlist applies. Every name the
|
|
787
|
-
// consolidation run allowlists must survive that gate, or the tool is
|
|
788
|
-
// silently missing from the run while the allowlist still claims it.
|
|
789
|
-
const consolidationCtx = () =>
|
|
790
|
-
makeCtx({
|
|
791
|
-
subagentAllowedTools: new Set(CONSOLIDATION_ALLOWED_TOOLS),
|
|
792
|
-
subagentToolGateMode: "wire",
|
|
793
|
-
});
|
|
794
|
-
|
|
795
|
-
test("every allowlisted tool is active for a background conversation", () => {
|
|
796
|
-
for (const name of CONSOLIDATION_ALLOWED_TOOLS) {
|
|
797
|
-
expect(isToolActiveForContext(name, consolidationCtx()), name).toBe(true);
|
|
798
|
-
}
|
|
799
|
-
});
|
|
800
|
-
|
|
801
|
-
test("the shell and the page-delete primitive are on the surface", () => {
|
|
802
|
-
expect(isToolActiveForContext("bash", consolidationCtx())).toBe(true);
|
|
803
|
-
expect(
|
|
804
|
-
isToolActiveForContext("delete_memory_page", consolidationCtx()),
|
|
805
|
-
).toBe(true);
|
|
806
|
-
});
|
|
807
|
-
|
|
808
|
-
test("network egress and host-proxy tools stay off the wire", () => {
|
|
809
|
-
for (const name of [
|
|
810
|
-
"web_fetch",
|
|
811
|
-
"web_search",
|
|
812
|
-
"network_request",
|
|
813
|
-
"host_bash",
|
|
814
|
-
"host_file_read",
|
|
815
|
-
"host_file_write",
|
|
816
|
-
"host_file_edit",
|
|
817
|
-
"host_cu",
|
|
818
|
-
]) {
|
|
819
|
-
expect(isToolActiveForContext(name, consolidationCtx()), name).toBe(
|
|
820
|
-
false,
|
|
821
|
-
);
|
|
822
|
-
}
|
|
823
|
-
});
|
|
824
|
-
});
|
|
@@ -38,7 +38,6 @@ import {
|
|
|
38
38
|
import { getConfig } from "../config/loader.js";
|
|
39
39
|
import type { LLMCallSite } from "../config/schemas/llm.js";
|
|
40
40
|
import { isSendUserMessageActiveForTurn } from "../config/send-user-message-gate.js";
|
|
41
|
-
import { desktopAutomationLease } from "../desktop/desktop-automation-lease.js";
|
|
42
41
|
import { writeRelationshipState } from "../home/relationship-state-writer.js";
|
|
43
42
|
import type { UserPromptSubmitInputContext } from "../hooks/types.js";
|
|
44
43
|
import {
|
|
@@ -914,7 +913,6 @@ export async function runAgentLoopImpl(
|
|
|
914
913
|
if (ctx.pendingReactionRecords?.length) {
|
|
915
914
|
ownedReactionRecords.push(...ctx.pendingReactionRecords.splice(0));
|
|
916
915
|
}
|
|
917
|
-
desktopAutomationLease.releaseForConversation(ctx.conversationId);
|
|
918
916
|
ctx.abortController = null;
|
|
919
917
|
ctx.setProcessing(false);
|
|
920
918
|
unregisterInflightTurn(ctx.conversationId, state);
|
|
@@ -22,10 +22,6 @@ import {
|
|
|
22
22
|
import { supportsChannelReaction } from "../messaging/providers/index.js";
|
|
23
23
|
import type { PermissionPrompter } from "../permissions/prompter.js";
|
|
24
24
|
import type { SecretPrompter } from "../permissions/secret-prompter.js";
|
|
25
|
-
import {
|
|
26
|
-
hashConversationToolSurface,
|
|
27
|
-
recordConversationToolSurface,
|
|
28
|
-
} from "../persistence/conversation-tool-surface.js";
|
|
29
25
|
import { getBindingByConversation } from "../persistence/external-conversation-store.js";
|
|
30
26
|
import { getAllDefaultPluginNames } from "../plugins/defaults/main.js";
|
|
31
27
|
import { isActivationSession } from "../plugins/defaults/memory/activation-session-store.js";
|
|
@@ -947,50 +943,6 @@ export function canSpawnSubagentsForTurn(ctx: Conversation): boolean {
|
|
|
947
943
|
);
|
|
948
944
|
}
|
|
949
945
|
|
|
950
|
-
/**
|
|
951
|
-
* Build the agent loop's `onToolsSent` observer for a conversation: record
|
|
952
|
-
* the tool array each provider call sends so a later fork wake can replay it
|
|
953
|
-
* (`recordConversationToolSurface`). Only the loop's send boundary sees the
|
|
954
|
-
* sent array. The resolver is also consulted out of band (the token count
|
|
955
|
-
* behind `/compact` and `/clean`, compaction estimates), where a read outside
|
|
956
|
-
* any turn resolves a clientless surface that would overwrite the one the
|
|
957
|
-
* conversation's turns actually send.
|
|
958
|
-
*
|
|
959
|
-
* Arrays that are not the conversation's own surface are skipped: a replaying
|
|
960
|
-
* wake sends its source's array, an empty array is a tools-disabled call (a
|
|
961
|
-
* fork replaying it could never call `remember`), and disk-pressure cleanup
|
|
962
|
-
* mode narrows the wire to cleanup tools. Best-effort: a failed write is
|
|
963
|
-
* logged and the array still counts as recorded, so a persistent failure logs
|
|
964
|
-
* once per distinct surface rather than once per provider call.
|
|
965
|
-
*/
|
|
966
|
-
export function createWireToolSurfaceRecorder(
|
|
967
|
-
ctx: Conversation,
|
|
968
|
-
): (tools: ToolDefinition[]) => void {
|
|
969
|
-
return (tools) => {
|
|
970
|
-
if (
|
|
971
|
-
!ctx.conversationId ||
|
|
972
|
-
ctx.wireToolReplay ||
|
|
973
|
-
tools.length === 0 ||
|
|
974
|
-
ctx.diskPressureCleanupModeActive === true
|
|
975
|
-
) {
|
|
976
|
-
return;
|
|
977
|
-
}
|
|
978
|
-
try {
|
|
979
|
-
ctx.recordedToolSurfaceHash = recordConversationToolSurface(
|
|
980
|
-
ctx.conversationId,
|
|
981
|
-
tools,
|
|
982
|
-
ctx.recordedToolSurfaceHash,
|
|
983
|
-
);
|
|
984
|
-
} catch (err) {
|
|
985
|
-
log.warn(
|
|
986
|
-
{ err, conversationId: ctx.conversationId },
|
|
987
|
-
"failed to record the conversation's wire tool surface; continuing",
|
|
988
|
-
);
|
|
989
|
-
ctx.recordedToolSurfaceHash = hashConversationToolSurface(tools);
|
|
990
|
-
}
|
|
991
|
-
};
|
|
992
|
-
}
|
|
993
|
-
|
|
994
946
|
/**
|
|
995
947
|
* Build a resolveTools callback that merges base tool definitions with
|
|
996
948
|
* dynamically projected skill tools on each agent turn. Also updates
|
|
@@ -1285,14 +1237,6 @@ export function createResolveToolsCallback(
|
|
|
1285
1237
|
|
|
1286
1238
|
ctx.allowedToolNames = turnAllowed;
|
|
1287
1239
|
|
|
1288
|
-
|
|
1289
|
-
// verbatim: the wire tool block is the first tier of the provider cache
|
|
1290
|
-
// prefix (tools → system → messages), so only the same bytes read the
|
|
1291
|
-
// source's cached prefix instead of rewriting it. Execution is unaffected:
|
|
1292
|
-
// `allowedToolNames` above and the executor's allowlist gate still decide
|
|
1293
|
-
// what may run.
|
|
1294
|
-
return ctx.wireToolReplay
|
|
1295
|
-
? [...ctx.wireToolReplay]
|
|
1296
|
-
: applyActivityField(allBaseDefs);
|
|
1240
|
+
return applyActivityField(allBaseDefs);
|
|
1297
1241
|
};
|
|
1298
1242
|
}
|
|
@@ -181,7 +181,6 @@ import {
|
|
|
181
181
|
canSpawnSubagentsForTurn,
|
|
182
182
|
createResolveToolsCallback,
|
|
183
183
|
createToolExecutor,
|
|
184
|
-
createWireToolSurfaceRecorder,
|
|
185
184
|
} from "./conversation-tool-setup.js";
|
|
186
185
|
import { canonicalizeTimeZone } from "./date-context.js";
|
|
187
186
|
import { HostAppControlProxy } from "./host-app-control-proxy.js";
|
|
@@ -453,21 +452,6 @@ export class Conversation {
|
|
|
453
452
|
* @internal
|
|
454
453
|
*/
|
|
455
454
|
toolContextPin?: WakeToolContextPin;
|
|
456
|
-
/**
|
|
457
|
-
* Tool definitions sent verbatim in place of the resolved wire array, for
|
|
458
|
-
* a wake replaying its source conversation's recorded surface
|
|
459
|
-
* (`recordConversationToolSurface`). Set and restored alongside the
|
|
460
|
-
* allowlist by `scopeWakeAllowedTools`; read only where the resolver returns
|
|
461
|
-
* the wire array, so it never widens what may execute.
|
|
462
|
-
* @internal
|
|
463
|
-
*/
|
|
464
|
-
wireToolReplay?: readonly ToolDefinition[];
|
|
465
|
-
/**
|
|
466
|
-
* Hash of the wire tool array last recorded for this conversation in this
|
|
467
|
-
* process, so an unchanged surface is not rewritten on every provider call.
|
|
468
|
-
* @internal
|
|
469
|
-
*/
|
|
470
|
-
recordedToolSurfaceHash?: string;
|
|
471
455
|
/** @internal */ readonly skillProjectionState = new Map<string, string>();
|
|
472
456
|
/** @internal */ readonly skillProjectionCache: SkillProjectionCache = {};
|
|
473
457
|
/** @internal */ usageStats: UsageStats = {
|
|
@@ -1054,7 +1038,6 @@ export class Conversation {
|
|
|
1054
1038
|
tools: toolDefs.length > 0 ? toolDefs : undefined,
|
|
1055
1039
|
toolExecutor: toolDefs.length > 0 ? toolExecutor : undefined,
|
|
1056
1040
|
resolveTools,
|
|
1057
|
-
onToolsSent: createWireToolSurfaceRecorder(this),
|
|
1058
1041
|
resolveConversationDir: () => {
|
|
1059
1042
|
const conv = getConversation(this.conversationId);
|
|
1060
1043
|
if (!conv) {
|
|
@@ -258,11 +258,15 @@ async function getDaemonStatus(): Promise<{
|
|
|
258
258
|
return { running: true, pid };
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
function getStartupLockPath(): string {
|
|
262
|
+
return getDaemonStartupLockPath();
|
|
263
|
+
}
|
|
264
|
+
|
|
261
265
|
/** Attempt to acquire a startup lock. Returns true on success. Stale locks
|
|
262
266
|
* (older than STARTUP_LOCK_STALE_MS) are forcibly removed to prevent
|
|
263
267
|
* permanent deadlocks from a crashed caller. */
|
|
264
268
|
function acquireStartupLock(): boolean {
|
|
265
|
-
const lockPath =
|
|
269
|
+
const lockPath = getStartupLockPath();
|
|
266
270
|
try {
|
|
267
271
|
// Ensure the root directory exists before attempting the lock file write.
|
|
268
272
|
// On a first-time run, getRootDir() may not exist yet, and writeFileSync
|
|
@@ -289,7 +293,7 @@ function acquireStartupLock(): boolean {
|
|
|
289
293
|
|
|
290
294
|
function releaseStartupLock(): void {
|
|
291
295
|
try {
|
|
292
|
-
unlinkSync(
|
|
296
|
+
unlinkSync(getStartupLockPath());
|
|
293
297
|
} catch {
|
|
294
298
|
/* already removed */
|
|
295
299
|
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Periodic reaper for orphaned subprocesses that reparent to the daemon.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* children stay attached and are torn down with `taskkill /T`. The
|
|
4
|
+
* Tools run commands in their own process group (`detached: true`) and, on
|
|
5
|
+
* timeout/abort, group-kill with `process.kill(-pgid, SIGKILL)` (the bash and
|
|
6
|
+
* host_bash tools, and the skill sandbox runner). The
|
|
8
7
|
* immediate child is reaped by Bun/libuv, but its descendants — e.g. git's
|
|
9
8
|
* transport helpers or a skill runner's `bun` process — were never spawned by
|
|
10
9
|
* the daemon, so when the group dies they reparent to PID 1. When the daemon
|
|
@@ -59,12 +59,6 @@ export interface SubagentToolStats {
|
|
|
59
59
|
* parity for desktop/web sources; channel-routed sources resolve every tool
|
|
60
60
|
* gate identically under `hasNoClient: true` with or without them.)
|
|
61
61
|
*
|
|
62
|
-
* A wake that also carries its source's recorded wire array
|
|
63
|
-
* (`Conversation.wireToolReplay`, set by `scopeWakeAllowedTools`) sends that
|
|
64
|
-
* array verbatim, so the pin then shapes only the execution-side inventory
|
|
65
|
-
* (`allowedToolNames`); the pin alone carries wire parity for a source with
|
|
66
|
-
* no recorded surface.
|
|
67
|
-
*
|
|
68
62
|
* Tool-definition resolution ONLY. The executor callback and host-proxy
|
|
69
63
|
* attachment paths never read the pin, so it cannot make a host tool
|
|
70
64
|
* runnable: in execution gate mode every non-allowlisted call is rejected
|