@oh-my-pi/pi-coding-agent 15.1.2 → 15.1.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/CHANGELOG.md +60 -0
- package/dist/types/async/job-manager.d.ts +3 -2
- package/dist/types/cli/auth-broker-cli.d.ts +25 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +18 -0
- package/dist/types/cli/grievances-cli.d.ts +12 -0
- package/dist/types/commands/auth-broker.d.ts +54 -0
- package/dist/types/commands/auth-gateway.d.ts +32 -0
- package/dist/types/commands/grievances.d.ts +1 -1
- package/dist/types/commit/agentic/tools/propose-commit.d.ts +9 -1
- package/dist/types/commit/agentic/tools/schemas.d.ts +9 -1
- package/dist/types/commit/agentic/tools/split-commit.d.ts +9 -1
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/models-config-schema.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +46 -0
- package/dist/types/discovery/agents.d.ts +12 -1
- package/dist/types/edit/renderer.d.ts +3 -0
- package/dist/types/eval/index.d.ts +0 -2
- package/dist/types/goals/tools/goal-tool.d.ts +10 -2
- package/dist/types/index.d.ts +0 -1
- package/dist/types/internal-urls/index.d.ts +1 -1
- package/dist/types/internal-urls/{pi-protocol.d.ts → omp-protocol.d.ts} +3 -3
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/main.d.ts +11 -2
- package/dist/types/modes/acp/acp-agent.d.ts +2 -1
- package/dist/types/modes/acp/acp-event-mapper.d.ts +13 -1
- package/dist/types/modes/acp/acp-mode.d.ts +3 -1
- package/dist/types/modes/emoji-autocomplete.d.ts +16 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +4 -0
- package/dist/types/plan-mode/approved-plan.d.ts +10 -4
- package/dist/types/sdk.d.ts +10 -3
- package/dist/types/session/agent-session.d.ts +7 -3
- package/dist/types/session/auth-broker-config.d.ts +13 -0
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/client-bridge.d.ts +3 -0
- package/dist/types/tools/eval.d.ts +41 -7
- package/dist/types/tools/irc.d.ts +8 -2
- package/dist/types/tools/report-tool-issue.d.ts +118 -1
- package/dist/types/tools/resolve.d.ts +8 -2
- package/examples/custom-tools/README.md +3 -12
- package/examples/extensions/README.md +2 -15
- package/examples/extensions/api-demo.ts +1 -7
- package/package.json +7 -7
- package/src/async/job-manager.ts +111 -13
- package/src/autoresearch/tools/init-experiment.ts +11 -33
- package/src/autoresearch/tools/log-experiment.ts +10 -24
- package/src/autoresearch/tools/run-experiment.ts +1 -1
- package/src/autoresearch/tools/update-notes.ts +2 -9
- package/src/cli/auth-broker-cli.ts +746 -0
- package/src/cli/auth-gateway-cli.ts +342 -0
- package/src/cli/grievances-cli.ts +109 -16
- package/src/cli/update-cli.ts +1 -5
- package/src/cli.ts +4 -2
- package/src/commands/auth-broker.ts +96 -0
- package/src/commands/auth-gateway.ts +61 -0
- package/src/commands/grievances.ts +13 -8
- package/src/commands/launch.ts +1 -1
- package/src/commit/agentic/agent.ts +2 -0
- package/src/commit/agentic/tools/analyze-file.ts +2 -2
- package/src/commit/agentic/tools/git-file-diff.ts +2 -2
- package/src/commit/agentic/tools/git-hunk.ts +3 -3
- package/src/commit/agentic/tools/git-overview.ts +2 -2
- package/src/commit/agentic/tools/propose-changelog.ts +1 -3
- package/src/commit/agentic/tools/recent-commits.ts +1 -1
- package/src/commit/agentic/tools/schemas.ts +1 -9
- package/src/config/model-equivalence.ts +279 -174
- package/src/config/model-registry.ts +37 -6
- package/src/config/model-resolver.ts +13 -8
- package/src/config/models-config-schema.ts +8 -0
- package/src/config/settings-schema.ts +52 -0
- package/src/cursor.ts +1 -1
- package/src/debug/log-formatting.ts +1 -1
- package/src/debug/log-viewer.ts +1 -1
- package/src/debug/profiler.ts +4 -0
- package/src/debug/raw-sse-buffer.ts +100 -59
- package/src/debug/raw-sse.ts +1 -1
- package/src/discovery/agents.ts +15 -4
- package/src/edit/modes/apply-patch.ts +1 -5
- package/src/edit/modes/patch.ts +5 -5
- package/src/edit/modes/replace.ts +5 -5
- package/src/edit/renderer.ts +2 -1
- package/src/edit/streaming.ts +1 -1
- package/src/eval/index.ts +0 -2
- package/src/eval/js/shared/runtime.ts +107 -2
- package/src/eval/py/kernel.ts +1 -1
- package/src/exa/researcher.ts +4 -4
- package/src/exa/search.ts +10 -22
- package/src/exa/websets.ts +33 -33
- package/src/extensibility/typebox.ts +44 -17
- package/src/goals/tools/goal-tool.ts +3 -3
- package/src/index.ts +0 -3
- package/src/internal-urls/docs-index.generated.ts +21 -18
- package/src/internal-urls/index.ts +1 -1
- package/src/internal-urls/{pi-protocol.ts → omp-protocol.ts} +10 -10
- package/src/internal-urls/router.ts +3 -3
- package/src/internal-urls/types.ts +1 -1
- package/src/lsp/types.ts +8 -11
- package/src/main.ts +216 -146
- package/src/mcp/tool-bridge.ts +3 -3
- package/src/modes/acp/acp-agent.ts +203 -57
- package/src/modes/acp/acp-client-bridge.ts +2 -1
- package/src/modes/acp/acp-event-mapper.ts +208 -32
- package/src/modes/acp/acp-mode.ts +11 -3
- package/src/modes/components/bash-execution.ts +1 -1
- package/src/modes/components/diff.ts +1 -2
- package/src/modes/components/eval-execution.ts +1 -1
- package/src/modes/components/oauth-selector.ts +38 -2
- package/src/modes/components/tool-execution.ts +1 -2
- package/src/modes/components/tree-selector.ts +26 -7
- package/src/modes/controllers/command-controller.ts +95 -34
- package/src/modes/controllers/input-controller.ts +4 -3
- package/src/modes/data/emojis.json +1 -0
- package/src/modes/emoji-autocomplete.ts +285 -0
- package/src/modes/interactive-mode.ts +92 -19
- package/src/modes/print-mode.ts +3 -3
- package/src/modes/prompt-action-autocomplete.ts +14 -0
- package/src/plan-mode/approved-plan.ts +30 -9
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/system/ttsr-tool-reminder.md +5 -0
- package/src/prompts/tools/ask.md +4 -3
- package/src/prompts/tools/eval.md +25 -26
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/resolve.md +1 -1
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/web-search.md +1 -1
- package/src/sdk.ts +81 -8
- package/src/session/agent-session.ts +362 -131
- package/src/session/agent-storage.ts +7 -2
- package/src/session/auth-broker-config.ts +102 -0
- package/src/session/auth-storage.ts +7 -1
- package/src/session/client-bridge.ts +3 -0
- package/src/session/streaming-output.ts +1 -1
- package/src/task/types.ts +10 -35
- package/src/tools/bash-interactive.ts +4 -1
- package/src/tools/bash-pty-selection.ts +2 -2
- package/src/tools/browser.ts +12 -20
- package/src/tools/eval.ts +77 -100
- package/src/tools/gh.ts +21 -45
- package/src/tools/hindsight-recall.ts +1 -1
- package/src/tools/hindsight-reflect.ts +2 -2
- package/src/tools/hindsight-retain.ts +3 -7
- package/src/tools/index.ts +8 -1
- package/src/tools/inspect-image.ts +4 -1
- package/src/tools/irc.ts +4 -12
- package/src/tools/job.ts +3 -11
- package/src/tools/report-tool-issue.ts +462 -17
- package/src/tools/resolve.ts +2 -7
- package/src/tools/todo-write.ts +8 -15
- package/src/utils/title-generator.ts +3 -0
- package/src/web/search/index.ts +6 -6
- package/dist/types/eval/parse.d.ts +0 -28
- package/dist/types/eval/sniff.d.ts +0 -11
- package/src/eval/eval.lark +0 -36
- package/src/eval/parse.ts +0 -407
- package/src/eval/sniff.ts +0 -28
package/src/mcp/tool-bridge.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
7
7
|
import type { TSchema } from "@oh-my-pi/pi-ai";
|
|
8
|
-
import {
|
|
8
|
+
import { normalizeSchemaForMCP } from "@oh-my-pi/pi-ai/utils/schema";
|
|
9
9
|
import { untilAborted } from "@oh-my-pi/pi-utils";
|
|
10
10
|
import type { SourceMeta } from "../capability/types";
|
|
11
11
|
import type {
|
|
@@ -231,7 +231,7 @@ export class MCPTool implements CustomTool<TSchema, MCPToolDetails> {
|
|
|
231
231
|
this.name = createMCPToolName(connection.name, tool.name);
|
|
232
232
|
this.label = `${connection.name}/${tool.name}`;
|
|
233
233
|
this.description = tool.description ?? `MCP tool from ${connection.name}`;
|
|
234
|
-
this.parameters =
|
|
234
|
+
this.parameters = normalizeSchemaForMCP(tool.inputSchema) as TSchema;
|
|
235
235
|
this.mcpToolName = tool.name;
|
|
236
236
|
this.mcpServerName = connection.name;
|
|
237
237
|
}
|
|
@@ -324,7 +324,7 @@ export class DeferredMCPTool implements CustomTool<TSchema, MCPToolDetails> {
|
|
|
324
324
|
this.name = createMCPToolName(serverName, tool.name);
|
|
325
325
|
this.label = `${serverName}/${tool.name}`;
|
|
326
326
|
this.description = tool.description ?? `MCP tool from ${serverName}`;
|
|
327
|
-
this.parameters =
|
|
327
|
+
this.parameters = normalizeSchemaForMCP(tool.inputSchema) as TSchema;
|
|
328
328
|
this.mcpToolName = tool.name;
|
|
329
329
|
this.mcpServerName = serverName;
|
|
330
330
|
this.#fallbackProvider = source?.provider;
|
|
@@ -66,7 +66,11 @@ import {
|
|
|
66
66
|
import { ACP_BUILTIN_SLASH_COMMANDS, executeAcpBuiltinSlashCommand } from "../../slash-commands/acp-builtins";
|
|
67
67
|
import { parseThinkingLevel } from "../../thinking";
|
|
68
68
|
import { createAcpClientBridge } from "./acp-client-bridge";
|
|
69
|
-
import {
|
|
69
|
+
import {
|
|
70
|
+
buildToolCallStartUpdate,
|
|
71
|
+
mapAgentSessionEventToAcpSessionUpdates,
|
|
72
|
+
normalizeReplayToolArguments,
|
|
73
|
+
} from "./acp-event-mapper";
|
|
70
74
|
import { ACP_TERMINAL_AUTH_FLAG } from "./terminal-auth";
|
|
71
75
|
|
|
72
76
|
const ACP_DEFAULT_MODE_ID = "default";
|
|
@@ -86,6 +90,9 @@ const SESSION_PAGE_SIZE = 50;
|
|
|
86
90
|
* wait past this guard without hard-coding the literal.
|
|
87
91
|
*/
|
|
88
92
|
export const ACP_BOOTSTRAP_RACE_GUARD_MS = 50;
|
|
93
|
+
const ACP_CANCEL_CLEANUP_TIMEOUT_MS = 5_000;
|
|
94
|
+
const ACP_ASYNC_DELIVERY_DRAIN_TIMEOUT_MS = 250;
|
|
95
|
+
const ACP_ASYNC_DELIVERY_DRAIN_MAX_PASSES = 3;
|
|
89
96
|
|
|
90
97
|
type AgentImageContent = {
|
|
91
98
|
type: "image";
|
|
@@ -102,6 +109,13 @@ type PromptTurnState = {
|
|
|
102
109
|
userMessageId: string;
|
|
103
110
|
cancelRequested: boolean;
|
|
104
111
|
settled: boolean;
|
|
112
|
+
/**
|
|
113
|
+
* `abort()` is in-flight (or its bounded-timeout race). `undefined` while the turn is
|
|
114
|
+
* running normally and after cleanup completes. The turn occupies `record.promptTurn`
|
|
115
|
+
* for as long as either `!settled` or `cleanup` is set — that combined window is the
|
|
116
|
+
* "turn in flight" predicate (`isPromptTurnInFlight`) every consumer gates on.
|
|
117
|
+
*/
|
|
118
|
+
cleanup: Promise<void> | undefined;
|
|
105
119
|
usageBaseline: UsageStatistics;
|
|
106
120
|
unsubscribe: (() => void) | undefined;
|
|
107
121
|
resolve: (value: PromptResponse) => void;
|
|
@@ -109,6 +123,16 @@ type PromptTurnState = {
|
|
|
109
123
|
promise: Promise<PromptResponse>;
|
|
110
124
|
};
|
|
111
125
|
|
|
126
|
+
/**
|
|
127
|
+
* A turn is "in flight" from the moment `prompt()` reserves the slot until `settled` is
|
|
128
|
+
* true AND any cancel cleanup has completed. Fork/queue/event gating all depend on this
|
|
129
|
+
* combined window — a settled-but-still-aborting turn is not safe to fork from, queue
|
|
130
|
+
* onto, or forward late events for.
|
|
131
|
+
*/
|
|
132
|
+
function isPromptTurnInFlight(turn: PromptTurnState | undefined): turn is PromptTurnState {
|
|
133
|
+
return turn !== undefined && (!turn.settled || turn.cleanup !== undefined);
|
|
134
|
+
}
|
|
135
|
+
|
|
112
136
|
type ManagedSessionRecord = {
|
|
113
137
|
session: AgentSession;
|
|
114
138
|
mcpManager: MCPManager | undefined;
|
|
@@ -116,6 +140,7 @@ type ManagedSessionRecord = {
|
|
|
116
140
|
promptQueue: PromptQueueState;
|
|
117
141
|
liveMessageId: string | undefined;
|
|
118
142
|
liveMessageProgress: { textEmitted: boolean; thoughtEmitted: boolean } | undefined;
|
|
143
|
+
toolArgsById: Map<string, unknown>;
|
|
119
144
|
extensionsConfigured: boolean;
|
|
120
145
|
// Installed inside `#scheduleBootstrapUpdates` (post-race-guard); released
|
|
121
146
|
// in `#disposeSessionRecord`. Lives independent of any prompt turn.
|
|
@@ -132,6 +157,14 @@ type ReplayableMessage = {
|
|
|
132
157
|
isError?: boolean;
|
|
133
158
|
};
|
|
134
159
|
|
|
160
|
+
type ReplayableToolItem = {
|
|
161
|
+
type?: unknown;
|
|
162
|
+
id?: unknown;
|
|
163
|
+
name?: unknown;
|
|
164
|
+
arguments?: unknown;
|
|
165
|
+
input?: unknown;
|
|
166
|
+
};
|
|
167
|
+
|
|
135
168
|
type MCPConfigMap = {
|
|
136
169
|
[name: string]: MCPServerConfig;
|
|
137
170
|
};
|
|
@@ -337,13 +370,18 @@ export class AcpAgent implements Agent {
|
|
|
337
370
|
#disposePromise: Promise<void> | undefined;
|
|
338
371
|
#cleanupRegistered = false;
|
|
339
372
|
#clientCapabilities: ClientCapabilities | undefined;
|
|
373
|
+
#cancelCleanupTimeoutMs = ACP_CANCEL_CLEANUP_TIMEOUT_MS;
|
|
340
374
|
|
|
341
|
-
constructor(connection: AgentSideConnection,
|
|
375
|
+
constructor(connection: AgentSideConnection, createSession: CreateAcpSession, initialSession?: AgentSession) {
|
|
342
376
|
this.#connection = connection;
|
|
343
377
|
this.#initialSession = initialSession;
|
|
344
378
|
this.#createSession = createSession;
|
|
345
379
|
}
|
|
346
380
|
|
|
381
|
+
setCancelCleanupTimeoutForTesting(timeoutMs: number): void {
|
|
382
|
+
this.#cancelCleanupTimeoutMs = Math.max(1, timeoutMs);
|
|
383
|
+
}
|
|
384
|
+
|
|
347
385
|
async initialize(params: InitializeRequest): Promise<InitializeResponse> {
|
|
348
386
|
this.#registerConnectionCleanup();
|
|
349
387
|
this.#clientCapabilities = params.clientCapabilities;
|
|
@@ -546,9 +584,15 @@ export class AcpAgent implements Agent {
|
|
|
546
584
|
throw new Error("ACP prompt already in progress for this session");
|
|
547
585
|
}
|
|
548
586
|
return await this.#queuePrompt(record, async () => {
|
|
549
|
-
const
|
|
550
|
-
if (
|
|
551
|
-
|
|
587
|
+
const previousTurn = record.promptTurn;
|
|
588
|
+
if (previousTurn) {
|
|
589
|
+
// Wait for any prompt that's still settling or whose cancel cleanup is
|
|
590
|
+
// still in flight. We deliberately swallow the prompt rejection (the
|
|
591
|
+
// owning caller already received it) but let cleanup rejections
|
|
592
|
+
// propagate — a timed-out cancel must fail this queued prompt instead
|
|
593
|
+
// of letting it run on a session that is about to be closed.
|
|
594
|
+
await previousTurn.promise.catch(() => undefined);
|
|
595
|
+
await previousTurn.cleanup;
|
|
552
596
|
}
|
|
553
597
|
|
|
554
598
|
const converted = this.#convertPromptBlocks(params.prompt);
|
|
@@ -557,6 +601,7 @@ export class AcpAgent implements Agent {
|
|
|
557
601
|
userMessageId: params.messageId ?? crypto.randomUUID(),
|
|
558
602
|
cancelRequested: false,
|
|
559
603
|
settled: false,
|
|
604
|
+
cleanup: undefined,
|
|
560
605
|
usageBaseline: this.#cloneUsageStatistics(record.session.sessionManager.getUsageStatistics()),
|
|
561
606
|
unsubscribe: undefined,
|
|
562
607
|
resolve: pendingPrompt.resolve,
|
|
@@ -604,7 +649,7 @@ export class AcpAgent implements Agent {
|
|
|
604
649
|
const builtinResult = await executeAcpBuiltinSlashCommand(text, {
|
|
605
650
|
session: record.session,
|
|
606
651
|
sessionManager: record.session.sessionManager,
|
|
607
|
-
settings:
|
|
652
|
+
settings: record.session.settings,
|
|
608
653
|
cwd: record.session.sessionManager.getCwd(),
|
|
609
654
|
output: output => this.#emitCommandOutput(record, output),
|
|
610
655
|
refreshCommands: () => this.#emitAvailableCommandsUpdate(record),
|
|
@@ -676,16 +721,53 @@ export class AcpAgent implements Agent {
|
|
|
676
721
|
if (!promptTurn || promptTurn.settled) {
|
|
677
722
|
return;
|
|
678
723
|
}
|
|
679
|
-
|
|
724
|
+
const cleanup = this.#beginCancelCleanup(record, promptTurn);
|
|
680
725
|
try {
|
|
681
|
-
await
|
|
682
|
-
this.#finishPrompt(record, {
|
|
683
|
-
stopReason: "cancelled",
|
|
684
|
-
usage: this.#buildTurnUsage(promptTurn.usageBaseline, record.session.sessionManager.getUsageStatistics()),
|
|
685
|
-
userMessageId: promptTurn.userMessageId,
|
|
686
|
-
});
|
|
726
|
+
await cleanup;
|
|
687
727
|
} catch (error: unknown) {
|
|
688
|
-
|
|
728
|
+
logger.warn("ACP cancel cleanup timed out; closing session", { sessionId: record.session.sessionId, error });
|
|
729
|
+
await this.#closeManagedSession(record.session.sessionId, record);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Transition a still-running turn into cancellation: mark intent, drop the live-event
|
|
735
|
+
* subscription, start the bounded `abort()` race, and resolve the ACP prompt response
|
|
736
|
+
* with `stopReason: "cancelled"` so the client sees acceptance immediately. The
|
|
737
|
+
* returned promise is the cleanup barrier — it resolves when `abort()` completes and
|
|
738
|
+
* rejects when the timeout fires. Idempotent: a second call returns the same barrier.
|
|
739
|
+
*/
|
|
740
|
+
#beginCancelCleanup(record: ManagedSessionRecord, promptTurn: PromptTurnState): Promise<void> {
|
|
741
|
+
if (promptTurn.cleanup) {
|
|
742
|
+
return promptTurn.cleanup;
|
|
743
|
+
}
|
|
744
|
+
promptTurn.cancelRequested = true;
|
|
745
|
+
promptTurn.unsubscribe?.();
|
|
746
|
+
const cleanup = this.#runCancelCleanup(record, promptTurn);
|
|
747
|
+
promptTurn.cleanup = cleanup;
|
|
748
|
+
this.#finishPrompt(record, {
|
|
749
|
+
stopReason: "cancelled",
|
|
750
|
+
usage: this.#buildTurnUsage(promptTurn.usageBaseline, record.session.sessionManager.getUsageStatistics()),
|
|
751
|
+
userMessageId: promptTurn.userMessageId,
|
|
752
|
+
});
|
|
753
|
+
return cleanup;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
async #runCancelCleanup(record: ManagedSessionRecord, promptTurn: PromptTurnState): Promise<void> {
|
|
757
|
+
let timer: NodeJS.Timeout | undefined;
|
|
758
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
759
|
+
timer = setTimeout(() => reject(new Error("ACP cancel cleanup timed out")), this.#cancelCleanupTimeoutMs);
|
|
760
|
+
});
|
|
761
|
+
try {
|
|
762
|
+
await Promise.race([record.session.abort(), timeout]);
|
|
763
|
+
} finally {
|
|
764
|
+
if (timer) clearTimeout(timer);
|
|
765
|
+
// Order matters: clear `cleanup` before evicting the slot so the slot-eviction
|
|
766
|
+
// branch matches what `#finishPrompt` saw if it ran first.
|
|
767
|
+
promptTurn.cleanup = undefined;
|
|
768
|
+
if (promptTurn.settled && record.promptTurn === promptTurn) {
|
|
769
|
+
record.promptTurn = undefined;
|
|
770
|
+
}
|
|
689
771
|
}
|
|
690
772
|
}
|
|
691
773
|
|
|
@@ -739,6 +821,9 @@ export class AcpAgent implements Agent {
|
|
|
739
821
|
case "_omp/usage": {
|
|
740
822
|
const [firstRecord] = this.#sessions.values();
|
|
741
823
|
const target = firstRecord?.session ?? this.#initialSession;
|
|
824
|
+
if (!target) {
|
|
825
|
+
return { reports: [] };
|
|
826
|
+
}
|
|
742
827
|
const reports = await target.fetchUsageReports();
|
|
743
828
|
return { reports: reports ?? [] };
|
|
744
829
|
}
|
|
@@ -891,6 +976,7 @@ export class AcpAgent implements Agent {
|
|
|
891
976
|
promptQueue: { promise: Promise.resolve(), release: undefined },
|
|
892
977
|
liveMessageId: undefined,
|
|
893
978
|
liveMessageProgress: undefined,
|
|
979
|
+
toolArgsById: new Map(),
|
|
894
980
|
extensionsConfigured: false,
|
|
895
981
|
lifetimeUnsubscribe: undefined,
|
|
896
982
|
};
|
|
@@ -929,8 +1015,7 @@ export class AcpAgent implements Agent {
|
|
|
929
1015
|
async #resolveForkSourceSessionPath(sessionId: string): Promise<string> {
|
|
930
1016
|
const loaded = this.#sessions.get(sessionId);
|
|
931
1017
|
if (loaded) {
|
|
932
|
-
|
|
933
|
-
if (promptTurn && !promptTurn.settled) {
|
|
1018
|
+
if (isPromptTurnInFlight(loaded.promptTurn)) {
|
|
934
1019
|
throw new Error(`ACP session fork is unavailable while a prompt is in progress: ${sessionId}`);
|
|
935
1020
|
}
|
|
936
1021
|
await loaded.session.sessionManager.flush();
|
|
@@ -950,23 +1035,31 @@ export class AcpAgent implements Agent {
|
|
|
950
1035
|
|
|
951
1036
|
async #handlePromptEvent(record: ManagedSessionRecord, event: AgentSessionEvent): Promise<void> {
|
|
952
1037
|
const promptTurn = record.promptTurn;
|
|
953
|
-
if (!promptTurn || promptTurn.settled) {
|
|
1038
|
+
if (!promptTurn || promptTurn.settled || promptTurn.cancelRequested) {
|
|
954
1039
|
return;
|
|
955
1040
|
}
|
|
956
1041
|
|
|
1042
|
+
if (event.type === "tool_execution_start" || event.type === "tool_execution_update") {
|
|
1043
|
+
record.toolArgsById.set(event.toolCallId, event.args);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
957
1046
|
this.#prepareLiveAssistantMessage(record, event);
|
|
958
1047
|
for (const notification of mapAgentSessionEventToAcpSessionUpdates(event, record.session.sessionId, {
|
|
959
1048
|
getMessageId: message => this.#getLiveMessageId(record, message),
|
|
960
1049
|
getMessageProgress: message => this.#getLiveMessageProgress(record, message),
|
|
1050
|
+
getToolArgs: toolCallId => record.toolArgsById.get(toolCallId),
|
|
961
1051
|
cwd: record.session.sessionManager.getCwd(),
|
|
962
1052
|
})) {
|
|
963
1053
|
await this.#connection.sessionUpdate(notification);
|
|
964
1054
|
}
|
|
1055
|
+
if (event.type === "tool_execution_end") {
|
|
1056
|
+
record.toolArgsById.delete(event.toolCallId);
|
|
1057
|
+
}
|
|
965
1058
|
this.#clearLiveAssistantMessageAfterEvent(record, event);
|
|
966
1059
|
|
|
967
1060
|
if (event.type === "agent_end") {
|
|
968
1061
|
await this.#emitEndOfTurnUpdates(record);
|
|
969
|
-
await record
|
|
1062
|
+
await this.#waitForAcpPromptIdle(record);
|
|
970
1063
|
this.#finishPrompt(record, {
|
|
971
1064
|
stopReason: this.#resolveStopReason(event, promptTurn.cancelRequested),
|
|
972
1065
|
usage: this.#buildTurnUsage(promptTurn.usageBaseline, record.session.sessionManager.getUsageStatistics()),
|
|
@@ -975,6 +1068,20 @@ export class AcpAgent implements Agent {
|
|
|
975
1068
|
}
|
|
976
1069
|
}
|
|
977
1070
|
|
|
1071
|
+
async #waitForAcpPromptIdle(record: ManagedSessionRecord): Promise<void> {
|
|
1072
|
+
for (let pass = 0; pass < ACP_ASYNC_DELIVERY_DRAIN_MAX_PASSES; pass++) {
|
|
1073
|
+
await record.session.waitForIdle();
|
|
1074
|
+
const delivered = await record.session.drainAsyncJobDeliveriesForAcp({
|
|
1075
|
+
timeoutMs: ACP_ASYNC_DELIVERY_DRAIN_TIMEOUT_MS,
|
|
1076
|
+
});
|
|
1077
|
+
if (!delivered) {
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
await record.session.waitForIdle();
|
|
1083
|
+
}
|
|
1084
|
+
|
|
978
1085
|
#prepareLiveAssistantMessage(record: ManagedSessionRecord, event: AgentSessionEvent): void {
|
|
979
1086
|
if (
|
|
980
1087
|
(event.type === "message_start" || event.type === "message_update" || event.type === "message_end") &&
|
|
@@ -1019,7 +1126,11 @@ export class AcpAgent implements Agent {
|
|
|
1019
1126
|
}
|
|
1020
1127
|
promptTurn.settled = true;
|
|
1021
1128
|
promptTurn.unsubscribe?.();
|
|
1022
|
-
|
|
1129
|
+
// Keep the slot occupied until cancel cleanup finishes — `#runCancelCleanup`
|
|
1130
|
+
// evicts the slot in its finally block once both flags say it's safe.
|
|
1131
|
+
if (!promptTurn.cleanup && record.promptTurn === promptTurn) {
|
|
1132
|
+
record.promptTurn = undefined;
|
|
1133
|
+
}
|
|
1023
1134
|
if (error !== undefined) {
|
|
1024
1135
|
promptTurn.reject(error);
|
|
1025
1136
|
return;
|
|
@@ -1227,7 +1338,7 @@ export class AcpAgent implements Agent {
|
|
|
1227
1338
|
|
|
1228
1339
|
#getAvailableModes(session: AgentSession): Array<{ id: string; name: string; description: string }> {
|
|
1229
1340
|
const modes = [{ id: ACP_DEFAULT_MODE_ID, name: "Default", description: "Standard ACP headless mode" }];
|
|
1230
|
-
if (
|
|
1341
|
+
if (session.settings.get("plan.enabled")) {
|
|
1231
1342
|
modes.push({
|
|
1232
1343
|
id: ACP_PLAN_MODE_ID,
|
|
1233
1344
|
name: "Plan",
|
|
@@ -1511,16 +1622,30 @@ export class AcpAgent implements Agent {
|
|
|
1511
1622
|
|
|
1512
1623
|
async #replaySessionHistory(record: ManagedSessionRecord): Promise<void> {
|
|
1513
1624
|
const cwd = record.session.sessionManager.getCwd();
|
|
1625
|
+
const replayedToolCallIds = new Set<string>();
|
|
1626
|
+
const replayedToolCallArgs = new Map<string, unknown>();
|
|
1514
1627
|
for (const message of record.session.sessionManager.buildSessionContext().messages as ReplayableMessage[]) {
|
|
1515
|
-
for (const notification of this.#messageToReplayNotifications(
|
|
1628
|
+
for (const notification of this.#messageToReplayNotifications(
|
|
1629
|
+
record.session.sessionId,
|
|
1630
|
+
message,
|
|
1631
|
+
cwd,
|
|
1632
|
+
replayedToolCallIds,
|
|
1633
|
+
replayedToolCallArgs,
|
|
1634
|
+
)) {
|
|
1516
1635
|
await this.#connection.sessionUpdate(notification);
|
|
1517
1636
|
}
|
|
1518
1637
|
}
|
|
1519
1638
|
}
|
|
1520
1639
|
|
|
1521
|
-
#messageToReplayNotifications(
|
|
1640
|
+
#messageToReplayNotifications(
|
|
1641
|
+
sessionId: string,
|
|
1642
|
+
message: ReplayableMessage,
|
|
1643
|
+
cwd: string,
|
|
1644
|
+
replayedToolCallIds: Set<string>,
|
|
1645
|
+
replayedToolCallArgs: Map<string, unknown>,
|
|
1646
|
+
): SessionNotification[] {
|
|
1522
1647
|
if (message.role === "assistant") {
|
|
1523
|
-
return this.#replayAssistantMessage(sessionId, message);
|
|
1648
|
+
return this.#replayAssistantMessage(sessionId, message, cwd, replayedToolCallIds, replayedToolCallArgs);
|
|
1524
1649
|
}
|
|
1525
1650
|
if (
|
|
1526
1651
|
message.role === "user" ||
|
|
@@ -1540,11 +1665,19 @@ export class AcpAgent implements Agent {
|
|
|
1540
1665
|
typeof message.toolCallId === "string" &&
|
|
1541
1666
|
typeof message.toolName === "string"
|
|
1542
1667
|
) {
|
|
1543
|
-
return this.#replayToolResult(
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1668
|
+
return this.#replayToolResult(
|
|
1669
|
+
sessionId,
|
|
1670
|
+
cwd,
|
|
1671
|
+
{
|
|
1672
|
+
...message,
|
|
1673
|
+
toolCallId: message.toolCallId,
|
|
1674
|
+
toolName: message.toolName,
|
|
1675
|
+
},
|
|
1676
|
+
{
|
|
1677
|
+
includeStart: !replayedToolCallIds.has(message.toolCallId),
|
|
1678
|
+
toolArgs: replayedToolCallArgs.get(message.toolCallId),
|
|
1679
|
+
},
|
|
1680
|
+
);
|
|
1548
1681
|
}
|
|
1549
1682
|
if (
|
|
1550
1683
|
message.role === "bashExecution" ||
|
|
@@ -1561,7 +1694,13 @@ export class AcpAgent implements Agent {
|
|
|
1561
1694
|
return [];
|
|
1562
1695
|
}
|
|
1563
1696
|
|
|
1564
|
-
#replayAssistantMessage(
|
|
1697
|
+
#replayAssistantMessage(
|
|
1698
|
+
sessionId: string,
|
|
1699
|
+
message: ReplayableMessage,
|
|
1700
|
+
cwd: string,
|
|
1701
|
+
replayedToolCallIds: Set<string>,
|
|
1702
|
+
replayedToolCallArgs: Map<string, unknown>,
|
|
1703
|
+
): SessionNotification[] {
|
|
1565
1704
|
const notifications: SessionNotification[] = [];
|
|
1566
1705
|
const messageId = crypto.randomUUID();
|
|
1567
1706
|
if (Array.isArray(message.content)) {
|
|
@@ -1596,24 +1735,23 @@ export class AcpAgent implements Agent {
|
|
|
1596
1735
|
});
|
|
1597
1736
|
continue;
|
|
1598
1737
|
}
|
|
1738
|
+
const toolItem = item as ReplayableToolItem;
|
|
1599
1739
|
if (
|
|
1600
|
-
(
|
|
1601
|
-
|
|
1602
|
-
typeof
|
|
1603
|
-
"name" in item &&
|
|
1604
|
-
typeof item.name === "string"
|
|
1740
|
+
(toolItem.type === "toolCall" || toolItem.type === "tool_use") &&
|
|
1741
|
+
typeof toolItem.id === "string" &&
|
|
1742
|
+
typeof toolItem.name === "string"
|
|
1605
1743
|
) {
|
|
1606
|
-
const
|
|
1607
|
-
|
|
1608
|
-
toolCallId:
|
|
1609
|
-
|
|
1610
|
-
|
|
1744
|
+
const args = this.#buildReplayAssistantToolArgs(toolItem);
|
|
1745
|
+
const update = buildToolCallStartUpdate({
|
|
1746
|
+
toolCallId: toolItem.id,
|
|
1747
|
+
toolName: toolItem.name,
|
|
1748
|
+
args,
|
|
1611
1749
|
status: "completed",
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
update.rawInput = item.arguments;
|
|
1615
|
-
}
|
|
1750
|
+
cwd,
|
|
1751
|
+
});
|
|
1616
1752
|
notifications.push({ sessionId, update });
|
|
1753
|
+
replayedToolCallIds.add(toolItem.id);
|
|
1754
|
+
replayedToolCallArgs.set(toolItem.id, args);
|
|
1617
1755
|
}
|
|
1618
1756
|
}
|
|
1619
1757
|
}
|
|
@@ -1630,10 +1768,21 @@ export class AcpAgent implements Agent {
|
|
|
1630
1768
|
return notifications;
|
|
1631
1769
|
}
|
|
1632
1770
|
|
|
1771
|
+
#buildReplayAssistantToolArgs(item: ReplayableToolItem): unknown {
|
|
1772
|
+
if ("arguments" in item) {
|
|
1773
|
+
return normalizeReplayToolArguments(item.arguments).args;
|
|
1774
|
+
}
|
|
1775
|
+
if (item.type === "tool_use" && "input" in item) {
|
|
1776
|
+
return item.input;
|
|
1777
|
+
}
|
|
1778
|
+
return {};
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1633
1781
|
#replayToolResult(
|
|
1634
1782
|
sessionId: string,
|
|
1635
1783
|
cwd: string,
|
|
1636
1784
|
message: Required<Pick<ReplayableMessage, "toolCallId" | "toolName">> & ReplayableMessage,
|
|
1785
|
+
options: { includeStart?: boolean; toolArgs?: unknown } = {},
|
|
1637
1786
|
): SessionNotification[] {
|
|
1638
1787
|
const args = this.#buildReplayToolArgs(message.details);
|
|
1639
1788
|
const startEvent: AgentSessionEvent = {
|
|
@@ -1653,10 +1802,14 @@ export class AcpAgent implements Agent {
|
|
|
1653
1802
|
errorMessage: message.errorMessage,
|
|
1654
1803
|
},
|
|
1655
1804
|
};
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1805
|
+
const notifications = mapAgentSessionEventToAcpSessionUpdates(endEvent, sessionId, {
|
|
1806
|
+
cwd,
|
|
1807
|
+
getToolArgs: toolCallId => (toolCallId === message.toolCallId ? options.toolArgs : undefined),
|
|
1808
|
+
});
|
|
1809
|
+
if (options.includeStart === false) {
|
|
1810
|
+
return notifications;
|
|
1811
|
+
}
|
|
1812
|
+
return [...mapAgentSessionEventToAcpSessionUpdates(startEvent, sessionId, { cwd }), ...notifications];
|
|
1660
1813
|
}
|
|
1661
1814
|
|
|
1662
1815
|
#buildReplayToolArgs(details: unknown): { path?: string } {
|
|
@@ -1887,22 +2040,15 @@ export class AcpAgent implements Agent {
|
|
|
1887
2040
|
|
|
1888
2041
|
async #cancelPromptForClose(record: ManagedSessionRecord): Promise<void> {
|
|
1889
2042
|
const promptTurn = record.promptTurn;
|
|
1890
|
-
if (!promptTurn
|
|
2043
|
+
if (!isPromptTurnInFlight(promptTurn)) {
|
|
1891
2044
|
return;
|
|
1892
2045
|
}
|
|
1893
|
-
|
|
1894
|
-
promptTurn.cancelRequested = true;
|
|
1895
|
-
promptTurn.unsubscribe?.();
|
|
2046
|
+
const cleanup = promptTurn.cleanup ?? this.#beginCancelCleanup(record, promptTurn);
|
|
1896
2047
|
try {
|
|
1897
|
-
await
|
|
2048
|
+
await cleanup;
|
|
1898
2049
|
} catch (error) {
|
|
1899
2050
|
logger.warn("Failed to abort ACP prompt during session close", { error });
|
|
1900
2051
|
}
|
|
1901
|
-
this.#finishPrompt(record, {
|
|
1902
|
-
stopReason: "cancelled",
|
|
1903
|
-
usage: this.#buildTurnUsage(promptTurn.usageBaseline, record.session.sessionManager.getUsageStatistics()),
|
|
1904
|
-
userMessageId: promptTurn.userMessageId,
|
|
1905
|
-
});
|
|
1906
2052
|
}
|
|
1907
2053
|
|
|
1908
2054
|
async #disposeSessionRecord(record: ManagedSessionRecord): Promise<void> {
|
|
@@ -36,7 +36,7 @@ export function createAcpClientBridge(
|
|
|
36
36
|
requestPermission: true,
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
const bridge: ClientBridge = { capabilities };
|
|
39
|
+
const bridge: ClientBridge = { capabilities, deferAgentInitiatedTurns: true };
|
|
40
40
|
|
|
41
41
|
if (capabilities.readTextFile) {
|
|
42
42
|
bridge.readTextFile = async params => {
|
|
@@ -122,6 +122,7 @@ async function requestPermission(
|
|
|
122
122
|
toolCallId: toolCall.toolCallId,
|
|
123
123
|
title: toolCall.title,
|
|
124
124
|
...(toolCall.kind ? { kind: toolCall.kind as ToolCallUpdate["kind"] } : {}),
|
|
125
|
+
...(toolCall.status ? { status: toolCall.status as ToolCallUpdate["status"] } : {}),
|
|
125
126
|
...(toolCall.rawInput !== undefined ? { rawInput: toolCall.rawInput } : {}),
|
|
126
127
|
...(toolCall.locations ? { locations: toolCall.locations } : {}),
|
|
127
128
|
};
|