@oh-my-pi/pi-coding-agent 17.0.7 → 17.0.9
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/CHANGELOG.md +91 -0
- package/dist/cli.js +5025 -4930
- package/dist/types/config/__tests__/model-registry.test.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +28 -1
- package/dist/types/config/model-resolver.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +50 -3
- package/dist/types/dap/client.d.ts +19 -0
- package/dist/types/extensibility/extensions/runner.d.ts +4 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +10 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +1 -1
- package/dist/types/extensibility/typebox.d.ts +4 -0
- package/dist/types/hindsight/client.d.ts +10 -0
- package/dist/types/hindsight/config.d.ts +8 -0
- package/dist/types/internal-urls/registry-helpers.d.ts +11 -0
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +6 -2
- package/dist/types/mcp/render.d.ts +2 -2
- package/dist/types/mcp/tool-bridge.d.ts +16 -3
- package/dist/types/mcp/types.d.ts +6 -0
- package/dist/types/mnemopi/state.d.ts +2 -0
- package/dist/types/modes/components/oauth-selector.d.ts +8 -0
- package/dist/types/modes/components/settings-defs.d.ts +1 -0
- package/dist/types/modes/components/user-message.d.ts +25 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +8 -0
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -1
- package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
- package/dist/types/modes/rpc/rpc-frame.d.ts +16 -0
- package/dist/types/modes/rpc/rpc-messages.d.ts +26 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +40 -0
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +9 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
- package/dist/types/modes/types.d.ts +14 -1
- package/dist/types/modes/utils/ui-helpers.d.ts +12 -8
- package/dist/types/sdk.d.ts +1 -0
- package/dist/types/session/agent-session-error-log.test.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +86 -3
- package/dist/types/session/session-loader.d.ts +6 -4
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/task/index.d.ts +1 -0
- package/dist/types/task/prewalk.d.ts +3 -0
- package/dist/types/task/types.d.ts +14 -0
- package/dist/types/tools/ask.d.ts +10 -0
- package/dist/types/tools/hub/jobs.d.ts +1 -1
- package/dist/types/tools/index.d.ts +3 -0
- package/dist/types/tools/report-tool-issue.d.ts +24 -9
- package/dist/types/tools/tool-timeouts.d.ts +8 -2
- package/dist/types/utils/changelog.d.ts +4 -6
- package/dist/types/web/search/providers/firecrawl.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +13 -13
- package/scripts/legacy-pi-virtual-module.ts +1 -1
- package/src/cli/grievances-cli.ts +2 -2
- package/src/cli/models-cli.ts +37 -17
- package/src/cli/update-cli.ts +14 -1
- package/src/cli/usage-cli.ts +1 -1
- package/src/config/__tests__/model-registry.test.ts +147 -0
- package/src/config/model-registry.ts +87 -24
- package/src/config/model-resolver.ts +56 -32
- package/src/config/settings-schema.ts +46 -3
- package/src/config/settings.ts +68 -5
- package/src/dap/client.ts +86 -7
- package/src/edit/diff.ts +4 -4
- package/src/eval/py/prelude.py +5 -5
- package/src/export/html/template.js +23 -9
- package/src/extensibility/extensions/runner.ts +9 -4
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +1 -0
- package/src/extensibility/legacy-pi-tui-shim.ts +10 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +851 -310
- package/src/extensibility/typebox.ts +22 -9
- package/src/hindsight/client.test.ts +42 -0
- package/src/hindsight/client.ts +40 -3
- package/src/hindsight/config.ts +18 -0
- package/src/internal-urls/registry-helpers.ts +40 -0
- package/src/launch/broker.ts +31 -5
- package/src/lsp/client.ts +23 -0
- package/src/lsp/clients/biome-client.ts +3 -4
- package/src/lsp/index.ts +26 -8
- package/src/main.ts +16 -5
- package/src/mcp/manager.ts +39 -8
- package/src/mcp/render.ts +94 -35
- package/src/mcp/tool-bridge.ts +116 -11
- package/src/mcp/types.ts +7 -0
- package/src/mnemopi/state.ts +70 -3
- package/src/modes/components/agent-dashboard.ts +6 -2
- package/src/modes/components/agent-hub.ts +26 -9
- package/src/modes/components/chat-transcript-builder.ts +13 -2
- package/src/modes/components/diff.ts +2 -2
- package/src/modes/components/oauth-selector.ts +24 -7
- package/src/modes/components/plan-review-overlay.ts +20 -4
- package/src/modes/components/settings-defs.ts +5 -2
- package/src/modes/components/settings-selector.ts +9 -5
- package/src/modes/components/tool-execution.test.ts +63 -2
- package/src/modes/components/user-message.ts +100 -1
- package/src/modes/controllers/command-controller.ts +36 -12
- package/src/modes/controllers/event-controller.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +18 -0
- package/src/modes/controllers/input-controller.ts +47 -12
- package/src/modes/controllers/mcp-command-controller.ts +70 -40
- package/src/modes/controllers/selector-controller.ts +82 -6
- package/src/modes/interactive-mode.ts +39 -4
- package/src/modes/rpc/rpc-client.ts +94 -3
- package/src/modes/rpc/rpc-frame.ts +164 -4
- package/src/modes/rpc/rpc-messages.ts +127 -0
- package/src/modes/rpc/rpc-mode.ts +79 -7
- package/src/modes/rpc/rpc-types.ts +34 -2
- package/src/modes/setup-wizard/scenes/model.ts +5 -7
- package/src/modes/setup-wizard/scenes/providers.ts +3 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +8 -2
- package/src/modes/setup-wizard/scenes/theme.ts +13 -3
- package/src/modes/setup-wizard/scenes/types.ts +9 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +6 -1
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/types.ts +16 -3
- package/src/modes/utils/ui-helpers.ts +40 -20
- package/src/prompts/goals/guided-goal-system.md +24 -3
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/task.md +12 -2
- package/src/sdk.ts +179 -51
- package/src/session/agent-session-error-log.test.ts +59 -0
- package/src/session/agent-session.ts +351 -46
- package/src/session/session-context.test.ts +224 -1
- package/src/session/session-context.ts +41 -2
- package/src/session/session-loader.ts +10 -5
- package/src/slash-commands/helpers/usage-report.ts +3 -1
- package/src/system-prompt.ts +1 -2
- package/src/task/executor.ts +44 -27
- package/src/task/index.ts +63 -8
- package/src/task/prewalk.ts +6 -0
- package/src/task/structured-subagent.ts +3 -1
- package/src/task/types.ts +16 -0
- package/src/task/worktree.ts +25 -11
- package/src/tools/ask.ts +15 -0
- package/src/tools/bash.ts +14 -6
- package/src/tools/browser.ts +1 -1
- package/src/tools/debug.ts +1 -1
- package/src/tools/eval.ts +4 -5
- package/src/tools/fetch.ts +1 -1
- package/src/tools/hub/index.ts +1 -1
- package/src/tools/hub/jobs.ts +67 -8
- package/src/tools/hub/launch.ts +7 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/report-tool-issue.ts +79 -28
- package/src/tools/tool-timeouts.ts +10 -3
- package/src/tools/write.ts +31 -0
- package/src/utils/changelog.ts +54 -58
- package/src/utils/git.ts +57 -49
- package/src/web/search/providers/firecrawl.ts +46 -13
- package/src/web/search/types.ts +5 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
3
|
+
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
3
4
|
import * as snapcompact from "@oh-my-pi/snapcompact";
|
|
4
|
-
import type
|
|
5
|
+
import { type CompactionSummaryMessage, INTERRUPTED_THINKING_MESSAGE_TYPE } from "./messages";
|
|
5
6
|
import { buildSessionContext, type StrippedToolCallsMarker } from "./session-context";
|
|
6
7
|
import type { SessionEntry } from "./session-entries";
|
|
7
8
|
|
|
@@ -159,3 +160,225 @@ describe("buildSessionContext dangling toolCalls", () => {
|
|
|
159
160
|
expect(context.messages.some(message => message.role === "assistant")).toBe(false);
|
|
160
161
|
});
|
|
161
162
|
});
|
|
163
|
+
|
|
164
|
+
const assistantUsage: AssistantMessage["usage"] = {
|
|
165
|
+
input: 0,
|
|
166
|
+
output: 0,
|
|
167
|
+
cacheRead: 0,
|
|
168
|
+
cacheWrite: 0,
|
|
169
|
+
totalTokens: 0,
|
|
170
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
function userEntry(id: string, parentId: string | null, content: string, messageTimestamp: number): SessionEntry {
|
|
174
|
+
return {
|
|
175
|
+
type: "message",
|
|
176
|
+
id,
|
|
177
|
+
parentId,
|
|
178
|
+
timestamp,
|
|
179
|
+
message: { role: "user", content, timestamp: messageTimestamp } as AgentMessage,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function assistantEntry(
|
|
184
|
+
id: string,
|
|
185
|
+
parentId: string | null,
|
|
186
|
+
stopReason: AssistantMessage["stopReason"],
|
|
187
|
+
text: string,
|
|
188
|
+
messageTimestamp: number,
|
|
189
|
+
): SessionEntry {
|
|
190
|
+
return {
|
|
191
|
+
type: "message",
|
|
192
|
+
id,
|
|
193
|
+
parentId,
|
|
194
|
+
timestamp,
|
|
195
|
+
message: {
|
|
196
|
+
role: "assistant",
|
|
197
|
+
content: [{ type: "text", text }],
|
|
198
|
+
api: "anthropic-messages",
|
|
199
|
+
provider: "anthropic",
|
|
200
|
+
model: "claude-sonnet-4-5",
|
|
201
|
+
usage: assistantUsage,
|
|
202
|
+
stopReason,
|
|
203
|
+
timestamp: messageTimestamp,
|
|
204
|
+
} satisfies AssistantMessage,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function toolCallAssistantEntry(
|
|
209
|
+
id: string,
|
|
210
|
+
parentId: string | null,
|
|
211
|
+
stopReason: AssistantMessage["stopReason"],
|
|
212
|
+
toolCallId: string,
|
|
213
|
+
messageTimestamp: number,
|
|
214
|
+
): SessionEntry {
|
|
215
|
+
return {
|
|
216
|
+
type: "message",
|
|
217
|
+
id,
|
|
218
|
+
parentId,
|
|
219
|
+
timestamp,
|
|
220
|
+
message: {
|
|
221
|
+
role: "assistant",
|
|
222
|
+
content: [{ type: "toolCall", id: toolCallId, name: "write", arguments: { path: "plan.md", content: "x" } }],
|
|
223
|
+
api: "anthropic-messages",
|
|
224
|
+
provider: "anthropic",
|
|
225
|
+
model: "claude-sonnet-4-5",
|
|
226
|
+
usage: assistantUsage,
|
|
227
|
+
stopReason,
|
|
228
|
+
timestamp: messageTimestamp,
|
|
229
|
+
} satisfies AssistantMessage,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function syntheticToolResultEntry(
|
|
234
|
+
id: string,
|
|
235
|
+
parentId: string | null,
|
|
236
|
+
toolCallId: string,
|
|
237
|
+
messageTimestamp: number,
|
|
238
|
+
): SessionEntry {
|
|
239
|
+
return {
|
|
240
|
+
type: "message",
|
|
241
|
+
id,
|
|
242
|
+
parentId,
|
|
243
|
+
timestamp,
|
|
244
|
+
message: {
|
|
245
|
+
role: "toolResult",
|
|
246
|
+
toolCallId,
|
|
247
|
+
toolName: "write",
|
|
248
|
+
content: [
|
|
249
|
+
{ type: "text", text: "Tool call was not executed because the provider stream ended with an error." },
|
|
250
|
+
],
|
|
251
|
+
details: { __synthetic: true, source: "assistant_stop_error", executed: false },
|
|
252
|
+
isError: true,
|
|
253
|
+
timestamp: messageTimestamp,
|
|
254
|
+
} as AgentMessage,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function hiddenContinuityEntry(id: string, parentId: string | null): SessionEntry {
|
|
259
|
+
return {
|
|
260
|
+
type: "custom_message",
|
|
261
|
+
id,
|
|
262
|
+
parentId,
|
|
263
|
+
timestamp,
|
|
264
|
+
customType: INTERRUPTED_THINKING_MESSAGE_TYPE,
|
|
265
|
+
content: "preserved interrupted thinking",
|
|
266
|
+
display: false,
|
|
267
|
+
attribution: "agent",
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function expectUserTail(messages: AgentMessage[], content: string): void {
|
|
272
|
+
const tail = messages.at(-1);
|
|
273
|
+
expect(tail?.role).toBe("user");
|
|
274
|
+
if (tail?.role !== "user") {
|
|
275
|
+
throw new Error(`Expected user tail, received ${tail?.role ?? "none"}`);
|
|
276
|
+
}
|
|
277
|
+
expect(tail.content).toBe(content);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
describe("buildSessionContext failed replay tails", () => {
|
|
281
|
+
it("terminates on cyclic parent links and includes each reachable message once", () => {
|
|
282
|
+
const entries = [userEntry("A", "B", "from A", 1), userEntry("B", "A", "from B", 2)];
|
|
283
|
+
|
|
284
|
+
const context = buildSessionContext(entries, "A");
|
|
285
|
+
|
|
286
|
+
expect(context.messages.map(message => (message.role === "user" ? message.content : message.role))).toEqual([
|
|
287
|
+
"from B",
|
|
288
|
+
"from A",
|
|
289
|
+
]);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("omits a terminal aborted assistant from normal context", () => {
|
|
293
|
+
const context = buildSessionContext([
|
|
294
|
+
userEntry("user", null, "continue", 1),
|
|
295
|
+
assistantEntry("assistant", "user", "aborted", "partial unsafe replay", 2),
|
|
296
|
+
]);
|
|
297
|
+
|
|
298
|
+
expect(context.messages.some(message => message.role === "assistant")).toBe(false);
|
|
299
|
+
expectUserTail(context.messages, "continue");
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("omits an earlier aborted assistant before a later user from normal context", () => {
|
|
303
|
+
const context = buildSessionContext([
|
|
304
|
+
userEntry("user-1", null, "first prompt", 1),
|
|
305
|
+
assistantEntry("assistant", "user-1", "aborted", "partial unsafe replay", 2),
|
|
306
|
+
userEntry("user-2", "assistant", "retry", 3),
|
|
307
|
+
]);
|
|
308
|
+
|
|
309
|
+
expect(context.messages.some(message => message.role === "assistant")).toBe(false);
|
|
310
|
+
expectUserTail(context.messages, "retry");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("preserves a terminal aborted assistant in transcript mode", () => {
|
|
314
|
+
const context = buildSessionContext(
|
|
315
|
+
[
|
|
316
|
+
userEntry("user", null, "continue", 1),
|
|
317
|
+
assistantEntry("assistant", "user", "aborted", "visible transcript error", 2),
|
|
318
|
+
],
|
|
319
|
+
undefined,
|
|
320
|
+
undefined,
|
|
321
|
+
{ transcript: true },
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const assistant = context.messages.find(message => message.role === "assistant");
|
|
325
|
+
expect(assistant?.role).toBe("assistant");
|
|
326
|
+
if (assistant?.role !== "assistant") {
|
|
327
|
+
throw new Error(`Expected transcript assistant, received ${assistant?.role ?? "none"}`);
|
|
328
|
+
}
|
|
329
|
+
expect(assistant.stopReason).toBe("aborted");
|
|
330
|
+
expect(assistant.content).toEqual([{ type: "text", text: "visible transcript error" }]);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("omits a terminal error assistant from normal context", () => {
|
|
334
|
+
const context = buildSessionContext([
|
|
335
|
+
userEntry("user", null, "retry with smaller input", 1),
|
|
336
|
+
assistantEntry("assistant", "user", "error", "provider rejected the request", 2),
|
|
337
|
+
]);
|
|
338
|
+
|
|
339
|
+
expect(context.messages.some(message => message.role === "assistant")).toBe(false);
|
|
340
|
+
expectUserTail(context.messages, "retry with smaller input");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("keeps an aborted assistant when hidden interrupted-thinking continuity follows it", () => {
|
|
344
|
+
const context = buildSessionContext([
|
|
345
|
+
userEntry("user", null, "keep reasoning continuity", 1),
|
|
346
|
+
assistantEntry("assistant", "user", "aborted", "partial answer before interrupt", 2),
|
|
347
|
+
hiddenContinuityEntry("continuity", "assistant"),
|
|
348
|
+
]);
|
|
349
|
+
|
|
350
|
+
const assistant = context.messages.find(message => message.role === "assistant");
|
|
351
|
+
expect(assistant?.role).toBe("assistant");
|
|
352
|
+
if (assistant?.role !== "assistant") {
|
|
353
|
+
throw new Error(`Expected assistant before continuity, received ${assistant?.role ?? "none"}`);
|
|
354
|
+
}
|
|
355
|
+
expect(assistant.stopReason).toBe("aborted");
|
|
356
|
+
expect(context.messages.at(-1)?.role).toBe("custom");
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("drops synthetic tool results paired with a dropped failed tool-call turn", () => {
|
|
360
|
+
const context = buildSessionContext([
|
|
361
|
+
userEntry("user", null, "write the plan", 1),
|
|
362
|
+
toolCallAssistantEntry("assistant", "user", "error", "call-1", 2),
|
|
363
|
+
syntheticToolResultEntry("result", "assistant", "call-1", 3),
|
|
364
|
+
]);
|
|
365
|
+
|
|
366
|
+
expect(context.messages.map(message => message.role)).toEqual(["user"]);
|
|
367
|
+
expectUserTail(context.messages, "write the plan");
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("keeps the failed tool-call turn and its result in transcript mode", () => {
|
|
371
|
+
const context = buildSessionContext(
|
|
372
|
+
[
|
|
373
|
+
userEntry("user", null, "write the plan", 1),
|
|
374
|
+
toolCallAssistantEntry("assistant", "user", "error", "call-1", 2),
|
|
375
|
+
syntheticToolResultEntry("result", "assistant", "call-1", 3),
|
|
376
|
+
],
|
|
377
|
+
undefined,
|
|
378
|
+
undefined,
|
|
379
|
+
{ transcript: true, keepDanglingToolCalls: true },
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
expect(context.messages.map(message => message.role)).toEqual(["user", "assistant", "toolResult"]);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
createBranchSummaryMessage,
|
|
6
6
|
createCompactionSummaryMessage,
|
|
7
7
|
createCustomMessage,
|
|
8
|
+
INTERRUPTED_THINKING_MESSAGE_TYPE,
|
|
8
9
|
isCustomMessageContent,
|
|
9
10
|
normalizeCustomMessagePayload,
|
|
10
11
|
} from "./messages";
|
|
@@ -197,10 +198,13 @@ export function buildSessionContext(
|
|
|
197
198
|
};
|
|
198
199
|
}
|
|
199
200
|
|
|
200
|
-
// Walk from leaf to root, collecting path
|
|
201
|
+
// Walk from leaf to root, collecting path. Corrupt/pre-fix files can contain
|
|
202
|
+
// parent cycles; stop at the first repeat so session load is bounded.
|
|
201
203
|
const path: SessionEntry[] = [];
|
|
204
|
+
const seenPathIds = new Set<string>();
|
|
202
205
|
let current: SessionEntry | undefined = leaf;
|
|
203
|
-
while (current) {
|
|
206
|
+
while (current && !seenPathIds.has(current.id)) {
|
|
207
|
+
seenPathIds.add(current.id);
|
|
204
208
|
path.push(current);
|
|
205
209
|
current = current.parentId ? byId.get(current.parentId) : undefined;
|
|
206
210
|
}
|
|
@@ -498,6 +502,41 @@ export function buildSessionContext(
|
|
|
498
502
|
}
|
|
499
503
|
}
|
|
500
504
|
|
|
505
|
+
// Error/abort assistant turns are transcript events, not safe assistant
|
|
506
|
+
// turns to replay into the next provider request. Drop them even when a
|
|
507
|
+
// later user message follows through non-context entries (`session_exit`,
|
|
508
|
+
// labels, etc.); otherwise a resumed session replays a dead partial turn
|
|
509
|
+
// and can spend minutes reprocessing old context before the new prompt.
|
|
510
|
+
// Keep the interrupted-thinking continuity pair: convertToLlm strips the
|
|
511
|
+
// unsafe trailing thinking from that assistant and sends the hidden
|
|
512
|
+
// continuity note instead.
|
|
513
|
+
if (!options?.transcript) {
|
|
514
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
515
|
+
const message = messages[i];
|
|
516
|
+
if (message?.role !== "assistant") continue;
|
|
517
|
+
if (message.stopReason !== "aborted" && message.stopReason !== "error") continue;
|
|
518
|
+
const next = messages[i + 1];
|
|
519
|
+
if (next?.role === "custom" && next.customType === INTERRUPTED_THINKING_MESSAGE_TYPE) continue;
|
|
520
|
+
// A failed turn that emitted tool calls persists paired synthetic
|
|
521
|
+
// tool_result placeholders after it. Dropping only the assistant would
|
|
522
|
+
// strand those results with no preceding tool_use — a shape providers
|
|
523
|
+
// reject — so remove the paired results alongside the turn.
|
|
524
|
+
const droppedToolCallIds = new Set<string>();
|
|
525
|
+
for (const block of message.content) {
|
|
526
|
+
if (block.type === "toolCall") droppedToolCallIds.add(block.id);
|
|
527
|
+
}
|
|
528
|
+
messages.splice(i, 1);
|
|
529
|
+
if (droppedToolCallIds.size > 0) {
|
|
530
|
+
for (let j = messages.length - 1; j >= i; j--) {
|
|
531
|
+
const candidate = messages[j];
|
|
532
|
+
if (candidate?.role === "toolResult" && droppedToolCallIds.has(candidate.toolCallId)) {
|
|
533
|
+
messages.splice(j, 1);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
501
540
|
return {
|
|
502
541
|
messages,
|
|
503
542
|
cacheMissExplainedAt: options?.transcript ? cacheMissExplainedAt : undefined,
|
|
@@ -306,10 +306,12 @@ export async function resolveBlobRefsInEntries(entries: FileEntry[], blobStore:
|
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
/**
|
|
309
|
-
* Read-only
|
|
310
|
-
* current version, resolve blob refs, and build the
|
|
311
|
-
* persisted leaf path (last entry).
|
|
312
|
-
*
|
|
309
|
+
* Read-only transcript view of a session file: load entries, migrate to the
|
|
310
|
+
* current version, resolve blob refs, and build the display transcript along
|
|
311
|
+
* the persisted leaf path (last entry). Uses transcript mode (collapsed to the
|
|
312
|
+
* latest compaction) so failed/aborted tail turns stay visible, unlike the
|
|
313
|
+
* provider-context builder which drops them. Does NOT create a writer or take
|
|
314
|
+
* the session lock — safe to call against a file another session is writing.
|
|
313
315
|
*/
|
|
314
316
|
export async function loadSessionMessagesReadOnly(filePath: string): Promise<AgentMessage[]> {
|
|
315
317
|
const entries = await loadEntriesFromFile(filePath);
|
|
@@ -317,5 +319,8 @@ export async function loadSessionMessagesReadOnly(filePath: string): Promise<Age
|
|
|
317
319
|
migrateToCurrentVersion(entries);
|
|
318
320
|
await resolveBlobRefsInEntries(entries, new BlobStore(getBlobsDir()));
|
|
319
321
|
const sessionEntries = entries.filter((e): e is SessionEntry => e.type !== "session");
|
|
320
|
-
return buildSessionContext(sessionEntries
|
|
322
|
+
return buildSessionContext(sessionEntries, undefined, undefined, {
|
|
323
|
+
transcript: true,
|
|
324
|
+
collapseCompactedHistory: true,
|
|
325
|
+
}).messages;
|
|
321
326
|
}
|
|
@@ -124,7 +124,9 @@ function renderUsageReports(
|
|
|
124
124
|
);
|
|
125
125
|
lines.push(` ${renderAsciiBar(limit.amount.usedFraction)}`);
|
|
126
126
|
if (limit.window?.resetsAt && limit.window.resetsAt > nowMs) {
|
|
127
|
-
lines.push(
|
|
127
|
+
lines.push(
|
|
128
|
+
` ${limit.window.resetLabel ?? "resets"} in ${formatDuration(limit.window.resetsAt - nowMs)}`,
|
|
129
|
+
);
|
|
128
130
|
}
|
|
129
131
|
if (limit.notes && limit.notes.length > 0)
|
|
130
132
|
lines.push(
|
package/src/system-prompt.ts
CHANGED
|
@@ -24,7 +24,6 @@ import projectPromptTemplate from "./prompts/system/project-prompt.md" with { ty
|
|
|
24
24
|
import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
|
|
25
25
|
import { normalizeConcurrencyLimit } from "./task/parallel";
|
|
26
26
|
import { usesCodexTaskPrompt } from "./task/prompt-policy";
|
|
27
|
-
import { shortenPath } from "./tools/render-utils";
|
|
28
27
|
import { type ActiveRepoContext, resolveActiveRepoContext } from "./utils/active-repo-context";
|
|
29
28
|
import { formatLocalCalendarDate } from "./utils/local-date";
|
|
30
29
|
import { normalizePromptPath } from "./utils/prompt-path";
|
|
@@ -710,7 +709,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
710
709
|
|
|
711
710
|
const date = formatLocalCalendarDate();
|
|
712
711
|
const dateTime = date;
|
|
713
|
-
const promptCwd =
|
|
712
|
+
const promptCwd = normalizePromptPath(resolvedCwd);
|
|
714
713
|
const activeRepoContextPrompt = renderActiveRepoContextPrompt(activeRepoContext);
|
|
715
714
|
|
|
716
715
|
// Build tool metadata for system prompt rendering.
|
package/src/task/executor.ts
CHANGED
|
@@ -30,7 +30,6 @@ import { getSessionSlashCommands } from "../extensibility/extensions/get-command
|
|
|
30
30
|
import { buildSkillPromptMessage, type Skill } from "../extensibility/skills";
|
|
31
31
|
import type { HindsightSessionState } from "../hindsight/state";
|
|
32
32
|
import type { LocalProtocolOptions } from "../internal-urls";
|
|
33
|
-
import { callTool } from "../mcp/client";
|
|
34
33
|
import type { MCPManager } from "../mcp/manager";
|
|
35
34
|
import type { MnemopiSessionState } from "../mnemopi/state";
|
|
36
35
|
import subagentSystemPromptTemplate from "../prompts/system/subagent-system-prompt.md" with { type: "text" };
|
|
@@ -55,6 +54,7 @@ import type { EventBus } from "../utils/event-bus";
|
|
|
55
54
|
import { buildNamedToolChoice } from "../utils/tool-choice";
|
|
56
55
|
import type { WorkspaceTree } from "../workspace-tree";
|
|
57
56
|
import { generateTaskLabel } from "./label";
|
|
57
|
+
import { resolveAgentPrewalkDefault } from "./prewalk";
|
|
58
58
|
import { subprocessToolRegistry } from "./subprocess-tool-registry";
|
|
59
59
|
import {
|
|
60
60
|
type AgentDefinition,
|
|
@@ -729,46 +729,54 @@ function getUsageTokens(usage: unknown): number {
|
|
|
729
729
|
|
|
730
730
|
/**
|
|
731
731
|
* Create proxy tools that reuse the parent's MCP connections.
|
|
732
|
+
*
|
|
733
|
+
* Each proxy delegates to the current source `MCPTool`/`DeferredMCPTool` rather
|
|
734
|
+
* than rebuilding a raw `tools/call` request, so the Task/subagent path shares
|
|
735
|
+
* the source tool's authoritative outbound boundary: harness-intent (`i`)
|
|
736
|
+
* stripping, optional-placeholder pruning, local-URL resolution, reconnect
|
|
737
|
+
* retry, abort handling, and result/provider metadata. The source tool is
|
|
738
|
+
* re-resolved on every call by raw MCP server/tool metadata (not the normalized
|
|
739
|
+
* display name), so a reconnect that swaps the instance in `getTools()` is
|
|
740
|
+
* always honored. The proxy adds only the Task-specific 60s call timeout,
|
|
741
|
+
* combining its abort signal with the caller's around source execution.
|
|
732
742
|
*/
|
|
733
743
|
export function createMCPProxyTools(mcpManager: MCPManager): CustomTool[] {
|
|
734
744
|
return mcpManager.getTools().map(tool => {
|
|
735
|
-
const
|
|
745
|
+
const serverName = tool.mcpServerName ?? "";
|
|
746
|
+
const mcpToolName = tool.mcpToolName ?? "";
|
|
736
747
|
return {
|
|
737
748
|
name: tool.name,
|
|
738
749
|
label: tool.label ?? tool.name,
|
|
739
750
|
description: tool.description ?? "",
|
|
740
751
|
parameters: tool.parameters,
|
|
741
|
-
|
|
752
|
+
strict: tool.strict,
|
|
753
|
+
mcpServerName: serverName,
|
|
754
|
+
mcpToolName,
|
|
755
|
+
execute: async (toolCallId, params, onUpdate, ctx, signal) => {
|
|
742
756
|
if (signal?.aborted) {
|
|
743
757
|
throw new ToolAbortError();
|
|
744
758
|
}
|
|
745
|
-
|
|
746
|
-
|
|
759
|
+
// Re-resolve by raw MCP metadata so a reconnect that replaced the
|
|
760
|
+
// source instance is picked up; the display name alone is not enough.
|
|
761
|
+
const source = mcpManager
|
|
762
|
+
.getTools()
|
|
763
|
+
.find(t => t.mcpServerName === serverName && t.mcpToolName === mcpToolName);
|
|
764
|
+
if (!source?.execute) {
|
|
765
|
+
return {
|
|
766
|
+
content: [{ type: "text" as const, text: `MCP error: tool ${mcpToolName} no longer available` }],
|
|
767
|
+
details: { serverName, mcpToolName, isError: true },
|
|
768
|
+
};
|
|
769
|
+
}
|
|
747
770
|
try {
|
|
748
771
|
const timeoutController = new AbortController();
|
|
749
772
|
const timeoutSignal = timeoutController.signal;
|
|
750
773
|
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
751
|
-
|
|
752
|
-
(
|
|
753
|
-
const connection = await untilAborted(combinedSignal, () =>
|
|
754
|
-
mcpManager.waitForConnection(serverName),
|
|
755
|
-
);
|
|
756
|
-
return callTool(connection, mcpToolName, params as Record<string, unknown>, {
|
|
757
|
-
signal: combinedSignal,
|
|
758
|
-
});
|
|
759
|
-
})(),
|
|
774
|
+
return await withAbortTimeout(
|
|
775
|
+
Promise.resolve(source.execute(toolCallId, params, onUpdate, ctx, combinedSignal)),
|
|
760
776
|
MCP_CALL_TIMEOUT_MS,
|
|
761
777
|
signal,
|
|
762
778
|
timeoutController,
|
|
763
779
|
);
|
|
764
|
-
return {
|
|
765
|
-
content: (result.content ?? []).map(item =>
|
|
766
|
-
item.type === "text"
|
|
767
|
-
? { type: "text" as const, text: item.text ?? "" }
|
|
768
|
-
: { type: "text" as const, text: JSON.stringify(item) },
|
|
769
|
-
),
|
|
770
|
-
details: { serverName, mcpToolName, isError: result.isError },
|
|
771
|
-
};
|
|
772
780
|
} catch (error) {
|
|
773
781
|
if (error instanceof ToolAbortError) {
|
|
774
782
|
throw error;
|
|
@@ -1519,9 +1527,16 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
1519
1527
|
scheduleProgress(flushProgress);
|
|
1520
1528
|
};
|
|
1521
1529
|
|
|
1522
|
-
const attach = (session: AgentSession): (() => void) =>
|
|
1523
|
-
session.
|
|
1530
|
+
const attach = (session: AgentSession): (() => void) => {
|
|
1531
|
+
let activeModel = session.model ? formatModelStringWithRouting(session.model) : undefined;
|
|
1532
|
+
return session.subscribe(event => {
|
|
1524
1533
|
emitSubagentEvent(event);
|
|
1534
|
+
const nextModel = session.model ? formatModelStringWithRouting(session.model) : undefined;
|
|
1535
|
+
if (nextModel && nextModel !== activeModel) {
|
|
1536
|
+
activeModel = nextModel;
|
|
1537
|
+
progress.resolvedModel = nextModel;
|
|
1538
|
+
scheduleProgress(true);
|
|
1539
|
+
}
|
|
1525
1540
|
if (event.type === "auto_retry_start") {
|
|
1526
1541
|
progress.retryState = {
|
|
1527
1542
|
attempt: event.attempt,
|
|
@@ -1563,15 +1578,18 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
1563
1578
|
}
|
|
1564
1579
|
if (event.type === "retry_fallback_applied") {
|
|
1565
1580
|
progress.resolvedModel = event.to;
|
|
1581
|
+
progress.resolvedModelIsFallback = true;
|
|
1566
1582
|
scheduleProgress(true);
|
|
1567
1583
|
return;
|
|
1568
1584
|
}
|
|
1569
1585
|
if (event.type === "retry_fallback_succeeded") {
|
|
1570
1586
|
progress.resolvedModel = event.model;
|
|
1587
|
+
progress.resolvedModelIsFallback = true;
|
|
1571
1588
|
scheduleProgress(true);
|
|
1572
1589
|
return;
|
|
1573
1590
|
}
|
|
1574
1591
|
});
|
|
1592
|
+
};
|
|
1575
1593
|
|
|
1576
1594
|
const captureSalvage = (session: AgentSession): void => {
|
|
1577
1595
|
// Best-effort salvage: capture the last assistant text so
|
|
@@ -1973,6 +1991,7 @@ async function finalizeRunResult(args: FinalizeRunArgs): Promise<SingleResult> {
|
|
|
1973
1991
|
contextWindow: progress.contextWindow,
|
|
1974
1992
|
modelOverride,
|
|
1975
1993
|
resolvedModel: progress.resolvedModel,
|
|
1994
|
+
resolvedModelIsFallback: progress.resolvedModelIsFallback,
|
|
1976
1995
|
error: exitCode !== 0 && stderr ? stderr : undefined,
|
|
1977
1996
|
aborted: wasAborted,
|
|
1978
1997
|
abortReason: finalAbortReason,
|
|
@@ -2451,11 +2470,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2451
2470
|
// frontmatter default; the `task.prewalk` toggle (default off) arms it.
|
|
2452
2471
|
// Resolution failures skip prewalk instead of failing the spawn.
|
|
2453
2472
|
let prewalk: Prewalk | undefined;
|
|
2454
|
-
const genericTaskPrewalk =
|
|
2455
|
-
agent.source === "bundled" && agent.name === "task" && settings.get("task.prewalk") ? true : undefined;
|
|
2456
2473
|
const prewalkPattern = resolveAgentPrewalkPattern({
|
|
2457
2474
|
settingsOverride: settings.get("task.agentPrewalk")[agent.name],
|
|
2458
|
-
agentPrewalk: agent.prewalk
|
|
2475
|
+
agentPrewalk: resolveAgentPrewalkDefault(agent, settings.get("task.prewalk")),
|
|
2459
2476
|
});
|
|
2460
2477
|
if (prewalkPattern) {
|
|
2461
2478
|
const resolvedPrewalk = resolveModelOverride([prewalkPattern], modelRegistry, settings);
|