@cjhyy/code-shell-core 0.8.2 → 0.8.4
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/dist/credentials/use-credential-tool.js +5 -2
- package/dist/engine/engine.js +17 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/llm/providers/openai.js +49 -2
- package/dist/protocol/chat-session-manager.js +9 -0
- package/dist/run/EngineRunner.js +5 -4
- package/dist/session/session-manager.d.ts +12 -13
- package/dist/session/session-manager.js +214 -28
- package/dist/session/transcript.d.ts +20 -4
- package/dist/session/transcript.js +97 -36
- package/dist/settings/schema.d.ts +99 -0
- package/dist/settings/schema.js +13 -0
- package/dist/tool-system/context.d.ts +6 -0
- package/dist/tool-system/external-tool-exposure.js +50 -26
- package/dist/tool-system/session-tool-host.d.ts +1 -1
- package/dist/tool-system/session-tool-host.js +31 -3
- package/dist/types.d.ts +7 -3
- package/package.json +1 -1
|
@@ -48,10 +48,18 @@ export declare class Transcript {
|
|
|
48
48
|
private filePath;
|
|
49
49
|
private currentTurn;
|
|
50
50
|
private readonly writer;
|
|
51
|
+
private readonly persistent;
|
|
51
52
|
private dirty;
|
|
52
53
|
private lastFlushFailure;
|
|
53
54
|
getFilePath(): string;
|
|
54
|
-
constructor(filePath: string, writer?: TranscriptWriter
|
|
55
|
+
constructor(filePath: string, writer?: TranscriptWriter, options?: {
|
|
56
|
+
persistent?: boolean;
|
|
57
|
+
});
|
|
58
|
+
/** A process-local transcript that never creates or appends a file. */
|
|
59
|
+
static inMemory(label: string): Transcript;
|
|
60
|
+
/** Rehydrate a process-local fork without serializing its copied history. */
|
|
61
|
+
static fromMemoryEvents(label: string, events: readonly TranscriptEvent[]): Transcript;
|
|
62
|
+
isPersistent(): boolean;
|
|
55
63
|
append(type: TranscriptEventType, data: Record<string, unknown>): TranscriptEvent;
|
|
56
64
|
/**
|
|
57
65
|
* Append a chat message to the transcript.
|
|
@@ -116,9 +124,16 @@ export declare class Transcript {
|
|
|
116
124
|
private errorErrno;
|
|
117
125
|
private errorMessage;
|
|
118
126
|
/**
|
|
119
|
-
*
|
|
120
|
-
* -
|
|
121
|
-
* -
|
|
127
|
+
* Normalize tool_result pairing in this detached in-memory snapshot:
|
|
128
|
+
* - orphaned results are removed;
|
|
129
|
+
* - duplicate results collapse to one, preferring a real result over the
|
|
130
|
+
* legacy synthetic interrupted placeholder;
|
|
131
|
+
* - missing results remain missing. The run-resume boundary patches those
|
|
132
|
+
* in its request-local Message[] after it has established ownership.
|
|
133
|
+
*
|
|
134
|
+
* This method must never append to the JSONL file. loadFromFile is used by
|
|
135
|
+
* read-only/background consumers while another run may be waiting for tool
|
|
136
|
+
* approval; persisting a synthetic result there races the real executor.
|
|
122
137
|
*/
|
|
123
138
|
repairToolResultPairs(): void;
|
|
124
139
|
static readEvents(filePath: string): ParsedEvents;
|
|
@@ -133,5 +148,6 @@ export declare class Transcript {
|
|
|
133
148
|
*/
|
|
134
149
|
static selectContextRange(events: readonly TranscriptEvent[], range: ContextEventRange): SelectedContextRange;
|
|
135
150
|
static loadFromFile(filePath: string): Transcript;
|
|
151
|
+
private loadEvents;
|
|
136
152
|
}
|
|
137
153
|
export {};
|
|
@@ -13,24 +13,84 @@ const CONTEXT_EVENT_TYPES = new Set([
|
|
|
13
13
|
"summary",
|
|
14
14
|
"context_transfer",
|
|
15
15
|
]);
|
|
16
|
+
const INTERRUPTED_TOOL_RESULT_ERROR = "[Tool result missing due to interrupted session]";
|
|
17
|
+
function isSyntheticInterruptedToolResult(event) {
|
|
18
|
+
return (event.type === "tool_result" &&
|
|
19
|
+
event.data.toolName === "unknown" &&
|
|
20
|
+
event.data.error === INTERRUPTED_TOOL_RESULT_ERROR);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Choose at most one result for every declared tool call. A real late result
|
|
24
|
+
* wins over the legacy synthetic "interrupted" placeholder that an older
|
|
25
|
+
* reader could persist while the tool was merely waiting for approval.
|
|
26
|
+
*/
|
|
27
|
+
function preferredToolResults(events) {
|
|
28
|
+
const toolUseIds = new Set();
|
|
29
|
+
for (const event of events) {
|
|
30
|
+
if (event.type === "tool_use" && typeof event.data.toolCallId === "string") {
|
|
31
|
+
toolUseIds.add(event.data.toolCallId);
|
|
32
|
+
}
|
|
33
|
+
if (event.type === "message" &&
|
|
34
|
+
event.data.role === "assistant" &&
|
|
35
|
+
Array.isArray(event.data.content)) {
|
|
36
|
+
for (const block of event.data.content) {
|
|
37
|
+
if (block.type === "tool_use" && typeof block.id === "string") {
|
|
38
|
+
toolUseIds.add(block.id);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const preferred = new Map();
|
|
44
|
+
for (const event of events) {
|
|
45
|
+
if (event.type !== "tool_result")
|
|
46
|
+
continue;
|
|
47
|
+
const toolCallId = event.data.toolCallId;
|
|
48
|
+
if (typeof toolCallId !== "string" || !toolUseIds.has(toolCallId))
|
|
49
|
+
continue;
|
|
50
|
+
const current = preferred.get(toolCallId);
|
|
51
|
+
if (!current ||
|
|
52
|
+
isSyntheticInterruptedToolResult(current) ||
|
|
53
|
+
!isSyntheticInterruptedToolResult(event)) {
|
|
54
|
+
preferred.set(toolCallId, event);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return preferred;
|
|
58
|
+
}
|
|
16
59
|
export class Transcript {
|
|
17
60
|
events = [];
|
|
18
61
|
filePath;
|
|
19
62
|
currentTurn = 0;
|
|
20
63
|
writer;
|
|
64
|
+
persistent;
|
|
21
65
|
dirty = false;
|
|
22
66
|
lastFlushFailure;
|
|
23
67
|
getFilePath() {
|
|
24
68
|
return this.filePath;
|
|
25
69
|
}
|
|
26
|
-
constructor(filePath, writer = appendFileSync) {
|
|
70
|
+
constructor(filePath, writer = appendFileSync, options = {}) {
|
|
27
71
|
this.filePath = filePath;
|
|
28
72
|
this.writer = writer;
|
|
73
|
+
this.persistent = options.persistent !== false;
|
|
74
|
+
if (!this.persistent)
|
|
75
|
+
return;
|
|
29
76
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
30
77
|
if (!existsSync(filePath)) {
|
|
31
78
|
writeFileSync(filePath, "", "utf-8");
|
|
32
79
|
}
|
|
33
80
|
}
|
|
81
|
+
/** A process-local transcript that never creates or appends a file. */
|
|
82
|
+
static inMemory(label) {
|
|
83
|
+
return new Transcript(`<memory:${label}>`, () => undefined, { persistent: false });
|
|
84
|
+
}
|
|
85
|
+
/** Rehydrate a process-local fork without serializing its copied history. */
|
|
86
|
+
static fromMemoryEvents(label, events) {
|
|
87
|
+
const transcript = Transcript.inMemory(label);
|
|
88
|
+
transcript.loadEvents(events);
|
|
89
|
+
return transcript;
|
|
90
|
+
}
|
|
91
|
+
isPersistent() {
|
|
92
|
+
return this.persistent;
|
|
93
|
+
}
|
|
34
94
|
append(type, data) {
|
|
35
95
|
const event = {
|
|
36
96
|
id: nanoid(12),
|
|
@@ -161,6 +221,7 @@ export class Transcript {
|
|
|
161
221
|
*/
|
|
162
222
|
toMessages() {
|
|
163
223
|
const messages = [];
|
|
224
|
+
const selectedToolResults = preferredToolResults(this.events);
|
|
164
225
|
for (const event of this.events) {
|
|
165
226
|
switch (event.type) {
|
|
166
227
|
case "message": {
|
|
@@ -174,6 +235,11 @@ export class Transcript {
|
|
|
174
235
|
break;
|
|
175
236
|
}
|
|
176
237
|
case "tool_result": {
|
|
238
|
+
const eventToolCallId = event.data.toolCallId;
|
|
239
|
+
if (typeof eventToolCallId !== "string" ||
|
|
240
|
+
selectedToolResults.get(eventToolCallId) !== event) {
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
177
243
|
const { toolCallId, result, error, contentBlocks } = event.data;
|
|
178
244
|
// Find if there's already a user message with tool_results to append to
|
|
179
245
|
const lastMsg = messages[messages.length - 1];
|
|
@@ -243,6 +309,8 @@ export class Transcript {
|
|
|
243
309
|
event.data.clientMessageId === clientMessageId);
|
|
244
310
|
}
|
|
245
311
|
flush(event) {
|
|
312
|
+
if (!this.persistent)
|
|
313
|
+
return true;
|
|
246
314
|
const line = JSON.stringify(event) + "\n";
|
|
247
315
|
try {
|
|
248
316
|
this.writer(this.filePath, line, "utf-8");
|
|
@@ -290,39 +358,24 @@ export class Transcript {
|
|
|
290
358
|
return error instanceof Error ? error.message : String(error);
|
|
291
359
|
}
|
|
292
360
|
/**
|
|
293
|
-
*
|
|
294
|
-
* -
|
|
295
|
-
* -
|
|
361
|
+
* Normalize tool_result pairing in this detached in-memory snapshot:
|
|
362
|
+
* - orphaned results are removed;
|
|
363
|
+
* - duplicate results collapse to one, preferring a real result over the
|
|
364
|
+
* legacy synthetic interrupted placeholder;
|
|
365
|
+
* - missing results remain missing. The run-resume boundary patches those
|
|
366
|
+
* in its request-local Message[] after it has established ownership.
|
|
367
|
+
*
|
|
368
|
+
* This method must never append to the JSONL file. loadFromFile is used by
|
|
369
|
+
* read-only/background consumers while another run may be waiting for tool
|
|
370
|
+
* approval; persisting a synthetic result there races the real executor.
|
|
296
371
|
*/
|
|
297
372
|
repairToolResultPairs() {
|
|
298
|
-
const
|
|
299
|
-
const toolResultIds = new Set();
|
|
300
|
-
for (const event of this.events) {
|
|
301
|
-
if (event.type === "tool_use") {
|
|
302
|
-
toolUseIds.add(event.data.toolCallId);
|
|
303
|
-
}
|
|
304
|
-
else if (event.type === "tool_result") {
|
|
305
|
-
toolResultIds.add(event.data.toolCallId);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
// Find tool_use events without matching tool_result
|
|
309
|
-
for (const id of toolUseIds) {
|
|
310
|
-
if (!toolResultIds.has(id)) {
|
|
311
|
-
// Synthesize an error result
|
|
312
|
-
this.append("tool_result", {
|
|
313
|
-
toolCallId: id,
|
|
314
|
-
toolName: "unknown",
|
|
315
|
-
error: "[Tool result missing due to interrupted session]",
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
// Remove orphaned tool_results (result without matching use)
|
|
373
|
+
const selectedToolResults = preferredToolResults(this.events);
|
|
320
374
|
this.events = this.events.filter((event) => {
|
|
321
|
-
if (event.type
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
return true;
|
|
375
|
+
if (event.type !== "tool_result")
|
|
376
|
+
return true;
|
|
377
|
+
const toolCallId = event.data.toolCallId;
|
|
378
|
+
return typeof toolCallId === "string" && selectedToolResults.get(toolCallId) === event;
|
|
326
379
|
});
|
|
327
380
|
}
|
|
328
381
|
static readEvents(filePath) {
|
|
@@ -436,22 +489,30 @@ export class Transcript {
|
|
|
436
489
|
return transcript;
|
|
437
490
|
const content = readFileSync(filePath, "utf-8");
|
|
438
491
|
const lines = content.split("\n").filter((l) => l.trim());
|
|
492
|
+
const events = [];
|
|
439
493
|
for (const line of lines) {
|
|
440
494
|
try {
|
|
441
|
-
|
|
442
|
-
transcript.events.push(event);
|
|
443
|
-
if (event.type === "turn_boundary") {
|
|
444
|
-
transcript.currentTurn = event.data.turnNumber ?? transcript.currentTurn + 1;
|
|
445
|
-
}
|
|
495
|
+
events.push(JSON.parse(line));
|
|
446
496
|
}
|
|
447
497
|
catch {
|
|
448
498
|
// Skip malformed lines
|
|
449
499
|
}
|
|
450
500
|
}
|
|
501
|
+
transcript.loadEvents(events);
|
|
451
502
|
// Repair pairing on load
|
|
452
503
|
transcript.repairToolResultPairs();
|
|
453
504
|
return transcript;
|
|
454
505
|
}
|
|
506
|
+
loadEvents(events) {
|
|
507
|
+
this.events = structuredClone([...events]);
|
|
508
|
+
this.currentTurn = 0;
|
|
509
|
+
for (const event of this.events) {
|
|
510
|
+
if (event.type === "turn_boundary") {
|
|
511
|
+
this.currentTurn =
|
|
512
|
+
typeof event.data.turnNumber === "number" ? event.data.turnNumber : this.currentTurn + 1;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
455
516
|
}
|
|
456
517
|
function isEngineResultReceipt(value) {
|
|
457
518
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -118,14 +118,47 @@ export declare const SettingsSchema: z.ZodObject<{
|
|
|
118
118
|
* Default on.
|
|
119
119
|
*/
|
|
120
120
|
memoryAutoExtract: z.ZodDefault<z.ZodBoolean>;
|
|
121
|
+
/**
|
|
122
|
+
* Mimi-only presentation and standing preferences. These are injected
|
|
123
|
+
* only into Pet manager turns; ordinary work Sessions continue to use
|
|
124
|
+
* the separate agent.* personalization fields above.
|
|
125
|
+
*/
|
|
126
|
+
personalization: z.ZodOptional<z.ZodObject<{
|
|
127
|
+
responseLanguage: z.ZodOptional<z.ZodString>;
|
|
128
|
+
userProfile: z.ZodOptional<z.ZodString>;
|
|
129
|
+
communicationStyle: z.ZodOptional<z.ZodString>;
|
|
130
|
+
customInstructions: z.ZodOptional<z.ZodString>;
|
|
131
|
+
}, "strip", z.ZodTypeAny, {
|
|
132
|
+
responseLanguage?: string | undefined;
|
|
133
|
+
userProfile?: string | undefined;
|
|
134
|
+
communicationStyle?: string | undefined;
|
|
135
|
+
customInstructions?: string | undefined;
|
|
136
|
+
}, {
|
|
137
|
+
responseLanguage?: string | undefined;
|
|
138
|
+
userProfile?: string | undefined;
|
|
139
|
+
communicationStyle?: string | undefined;
|
|
140
|
+
customInstructions?: string | undefined;
|
|
141
|
+
}>>;
|
|
121
142
|
}, "strip", z.ZodTypeAny, {
|
|
122
143
|
showExternalCodexSessions: boolean;
|
|
123
144
|
showExternalClaudeSessions: boolean;
|
|
124
145
|
memoryAutoExtract: boolean;
|
|
146
|
+
personalization?: {
|
|
147
|
+
responseLanguage?: string | undefined;
|
|
148
|
+
userProfile?: string | undefined;
|
|
149
|
+
communicationStyle?: string | undefined;
|
|
150
|
+
customInstructions?: string | undefined;
|
|
151
|
+
} | undefined;
|
|
125
152
|
}, {
|
|
126
153
|
showExternalCodexSessions?: boolean | undefined;
|
|
127
154
|
showExternalClaudeSessions?: boolean | undefined;
|
|
128
155
|
memoryAutoExtract?: boolean | undefined;
|
|
156
|
+
personalization?: {
|
|
157
|
+
responseLanguage?: string | undefined;
|
|
158
|
+
userProfile?: string | undefined;
|
|
159
|
+
communicationStyle?: string | undefined;
|
|
160
|
+
customInstructions?: string | undefined;
|
|
161
|
+
} | undefined;
|
|
129
162
|
}>>;
|
|
130
163
|
/**
|
|
131
164
|
* Image generation is a general capability, decoupled from LLM providers
|
|
@@ -1109,14 +1142,47 @@ export declare const SettingsSchema: z.ZodObject<{
|
|
|
1109
1142
|
* Default on.
|
|
1110
1143
|
*/
|
|
1111
1144
|
memoryAutoExtract: z.ZodDefault<z.ZodBoolean>;
|
|
1145
|
+
/**
|
|
1146
|
+
* Mimi-only presentation and standing preferences. These are injected
|
|
1147
|
+
* only into Pet manager turns; ordinary work Sessions continue to use
|
|
1148
|
+
* the separate agent.* personalization fields above.
|
|
1149
|
+
*/
|
|
1150
|
+
personalization: z.ZodOptional<z.ZodObject<{
|
|
1151
|
+
responseLanguage: z.ZodOptional<z.ZodString>;
|
|
1152
|
+
userProfile: z.ZodOptional<z.ZodString>;
|
|
1153
|
+
communicationStyle: z.ZodOptional<z.ZodString>;
|
|
1154
|
+
customInstructions: z.ZodOptional<z.ZodString>;
|
|
1155
|
+
}, "strip", z.ZodTypeAny, {
|
|
1156
|
+
responseLanguage?: string | undefined;
|
|
1157
|
+
userProfile?: string | undefined;
|
|
1158
|
+
communicationStyle?: string | undefined;
|
|
1159
|
+
customInstructions?: string | undefined;
|
|
1160
|
+
}, {
|
|
1161
|
+
responseLanguage?: string | undefined;
|
|
1162
|
+
userProfile?: string | undefined;
|
|
1163
|
+
communicationStyle?: string | undefined;
|
|
1164
|
+
customInstructions?: string | undefined;
|
|
1165
|
+
}>>;
|
|
1112
1166
|
}, "strip", z.ZodTypeAny, {
|
|
1113
1167
|
showExternalCodexSessions: boolean;
|
|
1114
1168
|
showExternalClaudeSessions: boolean;
|
|
1115
1169
|
memoryAutoExtract: boolean;
|
|
1170
|
+
personalization?: {
|
|
1171
|
+
responseLanguage?: string | undefined;
|
|
1172
|
+
userProfile?: string | undefined;
|
|
1173
|
+
communicationStyle?: string | undefined;
|
|
1174
|
+
customInstructions?: string | undefined;
|
|
1175
|
+
} | undefined;
|
|
1116
1176
|
}, {
|
|
1117
1177
|
showExternalCodexSessions?: boolean | undefined;
|
|
1118
1178
|
showExternalClaudeSessions?: boolean | undefined;
|
|
1119
1179
|
memoryAutoExtract?: boolean | undefined;
|
|
1180
|
+
personalization?: {
|
|
1181
|
+
responseLanguage?: string | undefined;
|
|
1182
|
+
userProfile?: string | undefined;
|
|
1183
|
+
communicationStyle?: string | undefined;
|
|
1184
|
+
customInstructions?: string | undefined;
|
|
1185
|
+
} | undefined;
|
|
1120
1186
|
}>>;
|
|
1121
1187
|
/**
|
|
1122
1188
|
* Image generation is a general capability, decoupled from LLM providers
|
|
@@ -2100,14 +2166,47 @@ export declare const SettingsSchema: z.ZodObject<{
|
|
|
2100
2166
|
* Default on.
|
|
2101
2167
|
*/
|
|
2102
2168
|
memoryAutoExtract: z.ZodDefault<z.ZodBoolean>;
|
|
2169
|
+
/**
|
|
2170
|
+
* Mimi-only presentation and standing preferences. These are injected
|
|
2171
|
+
* only into Pet manager turns; ordinary work Sessions continue to use
|
|
2172
|
+
* the separate agent.* personalization fields above.
|
|
2173
|
+
*/
|
|
2174
|
+
personalization: z.ZodOptional<z.ZodObject<{
|
|
2175
|
+
responseLanguage: z.ZodOptional<z.ZodString>;
|
|
2176
|
+
userProfile: z.ZodOptional<z.ZodString>;
|
|
2177
|
+
communicationStyle: z.ZodOptional<z.ZodString>;
|
|
2178
|
+
customInstructions: z.ZodOptional<z.ZodString>;
|
|
2179
|
+
}, "strip", z.ZodTypeAny, {
|
|
2180
|
+
responseLanguage?: string | undefined;
|
|
2181
|
+
userProfile?: string | undefined;
|
|
2182
|
+
communicationStyle?: string | undefined;
|
|
2183
|
+
customInstructions?: string | undefined;
|
|
2184
|
+
}, {
|
|
2185
|
+
responseLanguage?: string | undefined;
|
|
2186
|
+
userProfile?: string | undefined;
|
|
2187
|
+
communicationStyle?: string | undefined;
|
|
2188
|
+
customInstructions?: string | undefined;
|
|
2189
|
+
}>>;
|
|
2103
2190
|
}, "strip", z.ZodTypeAny, {
|
|
2104
2191
|
showExternalCodexSessions: boolean;
|
|
2105
2192
|
showExternalClaudeSessions: boolean;
|
|
2106
2193
|
memoryAutoExtract: boolean;
|
|
2194
|
+
personalization?: {
|
|
2195
|
+
responseLanguage?: string | undefined;
|
|
2196
|
+
userProfile?: string | undefined;
|
|
2197
|
+
communicationStyle?: string | undefined;
|
|
2198
|
+
customInstructions?: string | undefined;
|
|
2199
|
+
} | undefined;
|
|
2107
2200
|
}, {
|
|
2108
2201
|
showExternalCodexSessions?: boolean | undefined;
|
|
2109
2202
|
showExternalClaudeSessions?: boolean | undefined;
|
|
2110
2203
|
memoryAutoExtract?: boolean | undefined;
|
|
2204
|
+
personalization?: {
|
|
2205
|
+
responseLanguage?: string | undefined;
|
|
2206
|
+
userProfile?: string | undefined;
|
|
2207
|
+
communicationStyle?: string | undefined;
|
|
2208
|
+
customInstructions?: string | undefined;
|
|
2209
|
+
} | undefined;
|
|
2111
2210
|
}>>;
|
|
2112
2211
|
/**
|
|
2113
2212
|
* Image generation is a general capability, decoupled from LLM providers
|
package/dist/settings/schema.js
CHANGED
|
@@ -116,6 +116,19 @@ export const SettingsSchema = z
|
|
|
116
116
|
* Default on.
|
|
117
117
|
*/
|
|
118
118
|
memoryAutoExtract: z.boolean().default(true),
|
|
119
|
+
/**
|
|
120
|
+
* Mimi-only presentation and standing preferences. These are injected
|
|
121
|
+
* only into Pet manager turns; ordinary work Sessions continue to use
|
|
122
|
+
* the separate agent.* personalization fields above.
|
|
123
|
+
*/
|
|
124
|
+
personalization: z
|
|
125
|
+
.object({
|
|
126
|
+
responseLanguage: z.string().max(120).optional(),
|
|
127
|
+
userProfile: z.string().max(2_000).optional(),
|
|
128
|
+
communicationStyle: z.string().max(2_000).optional(),
|
|
129
|
+
customInstructions: z.string().max(6_000).optional(),
|
|
130
|
+
})
|
|
131
|
+
.optional(),
|
|
119
132
|
})
|
|
120
133
|
.default({}),
|
|
121
134
|
/**
|
|
@@ -225,6 +225,12 @@ export interface ToolRunYieldController {
|
|
|
225
225
|
export interface ToolContext {
|
|
226
226
|
/** Active working directory for this Engine. */
|
|
227
227
|
cwd: string;
|
|
228
|
+
/**
|
|
229
|
+
* True when this call belongs to an external Agent Runtime rather than the
|
|
230
|
+
* native Engine loop. Async handoff tools use this to keep their result on
|
|
231
|
+
* the current turn instead of queueing a wake-up only the Engine can consume.
|
|
232
|
+
*/
|
|
233
|
+
externalRuntime?: boolean;
|
|
228
234
|
/**
|
|
229
235
|
* Active digital-human profile's portable memory root. Present only when
|
|
230
236
|
* the resolved WorkspaceProfile enables portableMemory for this run.
|
|
@@ -9,15 +9,10 @@ export const FIRST_PHASE_EXPOSURE_RATIONALE = [
|
|
|
9
9
|
tool: "Panel",
|
|
10
10
|
kind: "host-loopback",
|
|
11
11
|
status: "exposed",
|
|
12
|
-
reason: "
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"the runtime starts), so the blocker is no longer technical. What is still " +
|
|
17
|
-
"missing is a per-Panel-App risk review — invoke runs third-party Panel App " +
|
|
18
|
-
"code with whatever arguments the model supplies, and argsPatterns cannot " +
|
|
19
|
-
"constrain a nested payload. Enabling it is a policy decision, not a wiring " +
|
|
20
|
-
"one, and it belongs to whoever reviews the first Panel App to be trusted.",
|
|
12
|
+
reason: "list/open/tools plus invoke for the reviewed job-hunt-hq tool catalog. The " +
|
|
13
|
+
"invoke exception is constrained by panel id and exact tool name; the Panel " +
|
|
14
|
+
"App manifest validates the nested payload. Other Panel Apps remain " +
|
|
15
|
+
"discovery/focus-only until they receive their own review.",
|
|
21
16
|
},
|
|
22
17
|
{
|
|
23
18
|
// NOTE the names: there is no tool called "Browser". The registry exposes
|
|
@@ -65,11 +60,10 @@ export const FIRST_PHASE_EXPOSURE_RATIONALE = [
|
|
|
65
60
|
"external runtime. If that approval is ever made skippable, this entry must " +
|
|
66
61
|
"go back to excluded.",
|
|
67
62
|
},
|
|
68
|
-
// ──
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
// that is advertised and then fails or misbehaves.
|
|
63
|
+
// ── Delegation and state-machine exceptions ──────────────────────
|
|
64
|
+
// Agent and the two plan-state tools remain structurally excluded. DriveAgent
|
|
65
|
+
// is the reviewed exception because the external host forces it into a
|
|
66
|
+
// foreground, one-level handoff with an observable result.
|
|
73
67
|
{
|
|
74
68
|
tool: "Agent",
|
|
75
69
|
kind: "self-contained",
|
|
@@ -82,8 +76,21 @@ export const FIRST_PHASE_EXPOSURE_RATIONALE = [
|
|
|
82
76
|
{
|
|
83
77
|
tool: "DriveAgent",
|
|
84
78
|
kind: "self-contained",
|
|
85
|
-
status: "
|
|
86
|
-
reason: "
|
|
79
|
+
status: "exposed",
|
|
80
|
+
reason: "Delegates one bounded task to an installed Codex/Claude CLI. External " +
|
|
81
|
+
"sessions force foreground execution and disable automatic background " +
|
|
82
|
+
"handoff, so the parent turn receives the result instead of losing a wake-up " +
|
|
83
|
+
"inside the native Engine queue. The child CLI does not inherit this host " +
|
|
84
|
+
"bridge, which bounds nesting at one level; the outer call still requires " +
|
|
85
|
+
"the normal DriveAgent approval.",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
tool: "DriveAgentJobs",
|
|
89
|
+
kind: "self-contained",
|
|
90
|
+
status: "exposed",
|
|
91
|
+
reason: "Lets the runtime inspect or cancel retained DriveAgent jobs. New external " +
|
|
92
|
+
"delegations run in the foreground, but retained jobs from the same Session " +
|
|
93
|
+
"still need an observable cleanup surface.",
|
|
87
94
|
},
|
|
88
95
|
{
|
|
89
96
|
tool: "EnterPlanMode",
|
|
@@ -218,17 +225,32 @@ export const FIRST_PHASE_EXPOSURE_RATIONALE = [
|
|
|
218
225
|
* This is a second, independent layer from the tool's own
|
|
219
226
|
* `defaultPermissionRules`: those decide allow/ask for a call that IS permitted,
|
|
220
227
|
* while this decides whether the action is reachable from an external runtime at
|
|
221
|
-
* all.
|
|
222
|
-
*
|
|
223
|
-
*
|
|
228
|
+
* all. `Panel.invoke` additionally fails closed on owner routing. Its exception
|
|
229
|
+
* here is limited to the reviewed job-hunt-hq panel and exact tool names; write
|
|
230
|
+
* operations still pass through Panel's schema, owner routing, permission rules,
|
|
231
|
+
* and approval handling.
|
|
224
232
|
*/
|
|
233
|
+
const JOB_HUNT_TOOL_NAMES = [
|
|
234
|
+
"get_job_search_context",
|
|
235
|
+
"save_candidate_context",
|
|
236
|
+
"save_job_opportunities",
|
|
237
|
+
"save_workflow_progress",
|
|
238
|
+
"save_job_research",
|
|
239
|
+
"save_interview_question_set",
|
|
240
|
+
"save_preparation_plan",
|
|
241
|
+
"save_interview_debrief",
|
|
242
|
+
"save_resume_draft",
|
|
243
|
+
].join("|");
|
|
244
|
+
const PANEL_ARGUMENT_PATTERNS = [
|
|
245
|
+
{ action: "list|open|tools" },
|
|
246
|
+
{
|
|
247
|
+
action: "invoke",
|
|
248
|
+
panel_id: "panel-app:job-hunt-hq",
|
|
249
|
+
tool_name: JOB_HUNT_TOOL_NAMES,
|
|
250
|
+
},
|
|
251
|
+
];
|
|
225
252
|
const FIRST_PHASE_ARGS_PATTERNS = new Map([
|
|
226
|
-
|
|
227
|
-
// Panel App code with whatever arguments the model supplies, and argsPatterns
|
|
228
|
-
// cannot constrain a nested payload — so unlike every tool widened above, the
|
|
229
|
-
// authorization layer genuinely cannot see what is being authorized. Enabling
|
|
230
|
-
// it belongs to whoever reviews the first Panel App to be trusted.
|
|
231
|
-
["Panel", { action: "list|open|tools" }],
|
|
253
|
+
["Panel", PANEL_ARGUMENT_PATTERNS],
|
|
232
254
|
]);
|
|
233
255
|
/**
|
|
234
256
|
* `ReadonlySet` / `ReadonlyMap` are compile-time only — the underlying `Set` and
|
|
@@ -299,6 +321,8 @@ export const FIRST_PHASE_EXPOSURE = Object.freeze({
|
|
|
299
321
|
toolNames: frozenSet(FIRST_PHASE_EXPOSURE_RATIONALE.filter((entry) => entry.status === "exposed").map((entry) => entry.tool)),
|
|
300
322
|
argsPatterns: frozenMap([...FIRST_PHASE_ARGS_PATTERNS].map(([tool, patterns]) => [
|
|
301
323
|
tool,
|
|
302
|
-
|
|
324
|
+
Array.isArray(patterns)
|
|
325
|
+
? Object.freeze(patterns.map((pattern) => Object.freeze({ ...pattern })))
|
|
326
|
+
: Object.freeze({ ...patterns }),
|
|
303
327
|
])),
|
|
304
328
|
});
|
|
@@ -61,7 +61,7 @@ export interface ExternalToolExposurePolicy {
|
|
|
61
61
|
* Panel App) is the tool's own schema validation, not this. Do not treat an
|
|
62
62
|
* `argsPatterns` entry as a sandbox for everything a tool might accept.
|
|
63
63
|
*/
|
|
64
|
-
argsPatterns?: ReadonlyMap<string, Readonly<Record<string, string
|
|
64
|
+
argsPatterns?: ReadonlyMap<string, Readonly<Record<string, string>> | readonly Readonly<Record<string, string>>[]>;
|
|
65
65
|
}
|
|
66
66
|
export interface SessionToolHost {
|
|
67
67
|
readonly businessSessionId: string;
|
|
@@ -3,6 +3,7 @@ import { PermissionClassifier } from "./permission.js";
|
|
|
3
3
|
import { HookRegistry } from "../hooks/registry.js";
|
|
4
4
|
import { buildToolVisibility } from "../engine/run-tooling.js";
|
|
5
5
|
import { composePermissionRules } from "../engine/permission-controller.js";
|
|
6
|
+
import { PLAN_MODE_ALLOWED_TOOLS } from "./plan-mode-allowlist.js";
|
|
6
7
|
const FORBIDDEN_MODES = new Set(["bypassPermissions", "dontAsk"]);
|
|
7
8
|
/**
|
|
8
9
|
* Whole-string match for an exposure pattern.
|
|
@@ -27,6 +28,10 @@ function matchesWholeValue(source, value) {
|
|
|
27
28
|
function argsMatch(patterns, input) {
|
|
28
29
|
if (!patterns)
|
|
29
30
|
return true;
|
|
31
|
+
const alternatives = Array.isArray(patterns) ? patterns : [patterns];
|
|
32
|
+
return alternatives.some((alternative) => argsPatternMatches(alternative, input));
|
|
33
|
+
}
|
|
34
|
+
function argsPatternMatches(patterns, input) {
|
|
30
35
|
for (const [key, source] of Object.entries(patterns)) {
|
|
31
36
|
// Read own properties only: a model-supplied JSON body cannot smuggle a
|
|
32
37
|
// match through the prototype chain.
|
|
@@ -81,6 +86,7 @@ export function createSessionToolHost(options) {
|
|
|
81
86
|
// just rejected.
|
|
82
87
|
...options.contextOverrides,
|
|
83
88
|
sessionId: options.businessSessionId,
|
|
89
|
+
externalRuntime: true,
|
|
84
90
|
planMode: options.planMode,
|
|
85
91
|
permissionMode: options.permissionMode,
|
|
86
92
|
toolVisibility: buildToolVisibility(options.visibility),
|
|
@@ -106,7 +112,12 @@ export function createSessionToolHost(options) {
|
|
|
106
112
|
return [];
|
|
107
113
|
return registry
|
|
108
114
|
.getToolDefinitions()
|
|
109
|
-
.filter((definition) => exposure.toolNames.has(definition.name))
|
|
115
|
+
.filter((definition) => exposure.toolNames.has(definition.name))
|
|
116
|
+
.filter((definition) => !options.planMode || PLAN_MODE_ALLOWED_TOOLS.has(definition.name))
|
|
117
|
+
.filter((definition) => {
|
|
118
|
+
const guard = registry.getAvailabilityGuard(definition.name);
|
|
119
|
+
return !guard || guard(toolCtx.toolVisibility);
|
|
120
|
+
});
|
|
110
121
|
},
|
|
111
122
|
async execute(call, callSignal) {
|
|
112
123
|
if (disposed) {
|
|
@@ -129,6 +140,19 @@ export function createSessionToolHost(options) {
|
|
|
129
140
|
if (sessionSignal.aborted || callSignal?.aborted) {
|
|
130
141
|
return failClosed(call.id, call.name, `Tool aborted before execution: ${call.name}`);
|
|
131
142
|
}
|
|
143
|
+
const emit = async (event) => {
|
|
144
|
+
try {
|
|
145
|
+
await toolCtx.streamCallback?.(event);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Stream projection is observational; a broken renderer/recorder must
|
|
149
|
+
// not turn an authorized tool execution into an unrelated failure.
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
await emit({
|
|
153
|
+
type: "tool_use_start",
|
|
154
|
+
toolCall: { id: call.id, toolName: call.name, args: input },
|
|
155
|
+
});
|
|
132
156
|
// Forward the per-call signal rather than only checking it at entry: an MCP
|
|
133
157
|
// cancellation arriving mid-flight has to actually stop the work.
|
|
134
158
|
//
|
|
@@ -139,20 +163,24 @@ export function createSessionToolHost(options) {
|
|
|
139
163
|
// state. Calls without one use the shared executor unchanged.
|
|
140
164
|
if (!callSignal) {
|
|
141
165
|
// The one authorized path. Everything above only NARROWS what may reach it.
|
|
142
|
-
|
|
166
|
+
const result = await executor.executeSingle({
|
|
143
167
|
id: call.id,
|
|
144
168
|
toolName: call.name,
|
|
145
169
|
args: input,
|
|
146
170
|
});
|
|
171
|
+
await emit({ type: "tool_result", result });
|
|
172
|
+
return result;
|
|
147
173
|
}
|
|
148
174
|
const scoped = new ToolExecutor(registry, permission, hooks);
|
|
149
175
|
scoped.setContext(toolCtx);
|
|
150
176
|
scoped.setSignal(AbortSignal.any([sessionSignal, callSignal]));
|
|
151
|
-
|
|
177
|
+
const result = await scoped.executeSingle({
|
|
152
178
|
id: call.id,
|
|
153
179
|
toolName: call.name,
|
|
154
180
|
args: input,
|
|
155
181
|
});
|
|
182
|
+
await emit({ type: "tool_result", result });
|
|
183
|
+
return result;
|
|
156
184
|
},
|
|
157
185
|
async dispose() {
|
|
158
186
|
// Order matters (§13.4): stop accepting new calls, THEN abort in-flight
|
package/dist/types.d.ts
CHANGED
|
@@ -298,9 +298,10 @@ export interface SessionState {
|
|
|
298
298
|
/** User-fork lineage; deliberately separate from sub-agent ownership. */
|
|
299
299
|
forkedFrom?: SessionForkLineage;
|
|
300
300
|
/**
|
|
301
|
-
* Temporary child sessions
|
|
302
|
-
*
|
|
303
|
-
*
|
|
301
|
+
* Temporary child sessions live only in process memory. Ordinary
|
|
302
|
+
* resume/session pickers omit them, and closing/expiry forgets them instead
|
|
303
|
+
* of publishing state or transcript files. Absent on legacy sessions;
|
|
304
|
+
* desktop also recognizes its historical `qchat-*` namespace.
|
|
304
305
|
*/
|
|
305
306
|
ephemeral?: boolean;
|
|
306
307
|
/**
|
|
@@ -636,6 +637,9 @@ export type StreamEvent = {
|
|
|
636
637
|
sessionCacheReadTokens?: number;
|
|
637
638
|
sessionCacheCreationTokens?: number;
|
|
638
639
|
sessionPromptTokens?: number;
|
|
640
|
+
/** Provider-reported completion tokens for the latest and whole thread. */
|
|
641
|
+
completionTokens?: number;
|
|
642
|
+
cumulativeCompletionTokens?: number;
|
|
639
643
|
agentId?: string;
|
|
640
644
|
} | {
|
|
641
645
|
type: "background_agent_completed";
|