@oh-my-pi/pi-coding-agent 15.4.3 → 15.5.0
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 +75 -5
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/auth-broker-cli.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +8 -0
- package/dist/types/config/settings-schema.d.ts +42 -1
- package/dist/types/edit/index.d.ts +2 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +8 -2
- package/dist/types/extensibility/hooks/types.d.ts +4 -0
- package/dist/types/lsp/index.d.ts +9 -1
- package/dist/types/mcp/client.d.ts +2 -1
- package/dist/types/mcp/oauth-discovery.d.ts +4 -3
- package/dist/types/mcp/timeout.d.ts +9 -0
- package/dist/types/mcp/types.d.ts +1 -1
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/streaming-output.d.ts +1 -1
- package/dist/types/task/index.d.ts +2 -0
- package/dist/types/task/types.d.ts +4 -0
- package/dist/types/tools/approval.d.ts +46 -0
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/ast-edit.d.ts +2 -0
- package/dist/types/tools/ast-grep.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +11 -1
- package/dist/types/tools/browser.d.ts +2 -0
- package/dist/types/tools/calculator.d.ts +1 -0
- package/dist/types/tools/checkpoint.d.ts +2 -0
- package/dist/types/tools/debug.d.ts +9 -1
- package/dist/types/tools/eval.d.ts +2 -0
- package/dist/types/tools/find.d.ts +10 -0
- package/dist/types/tools/gh.d.ts +2 -1
- package/dist/types/tools/hindsight-recall.d.ts +1 -0
- package/dist/types/tools/hindsight-reflect.d.ts +1 -0
- package/dist/types/tools/hindsight-retain.d.ts +1 -0
- package/dist/types/tools/inspect-image.d.ts +1 -0
- package/dist/types/tools/irc.d.ts +1 -0
- package/dist/types/tools/job.d.ts +1 -0
- package/dist/types/tools/read.d.ts +1 -0
- package/dist/types/tools/recipe/index.d.ts +1 -0
- package/dist/types/tools/render-mermaid.d.ts +1 -0
- package/dist/types/tools/resolve.d.ts +1 -0
- package/dist/types/tools/search-tool-bm25.d.ts +1 -0
- package/dist/types/tools/search.d.ts +1 -0
- package/dist/types/tools/ssh.d.ts +2 -0
- package/dist/types/tools/todo-write.d.ts +1 -0
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/tools/yield.d.ts +1 -0
- package/dist/types/web/search/index.d.ts +1 -0
- package/package.json +7 -7
- package/src/cli/args.ts +14 -0
- package/src/cli/auth-broker-cli.ts +171 -22
- package/src/commands/auth-broker.ts +3 -0
- package/src/commands/launch.ts +16 -0
- package/src/config/mcp-schema.json +2 -2
- package/src/config/model-registry.ts +19 -4
- package/src/config/settings-schema.ts +59 -1
- package/src/config/settings.ts +2 -1
- package/src/dap/session.ts +35 -2
- package/src/discovery/builtin.ts +2 -2
- package/src/discovery/mcp-json.ts +1 -1
- package/src/edit/index.ts +26 -0
- package/src/edit/modes/patch.ts +1 -1
- package/src/edit/streaming.ts +12 -2
- package/src/exec/bash-executor.ts +6 -2
- package/src/extensibility/custom-commands/bundled/review/index.ts +18 -14
- package/src/extensibility/custom-tools/types.ts +16 -2
- package/src/extensibility/extensions/wrapper.ts +36 -1
- package/src/extensibility/hooks/types.ts +8 -1
- package/src/hashline/apply.ts +47 -2
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/lsp/edits.ts +82 -29
- package/src/lsp/index.ts +38 -1
- package/src/lsp/utils.ts +1 -1
- package/src/main.ts +6 -0
- package/src/mcp/client.ts +8 -6
- package/src/mcp/oauth-discovery.ts +120 -32
- package/src/mcp/oauth-flow.ts +34 -6
- package/src/mcp/timeout.ts +59 -0
- package/src/mcp/transports/http.ts +42 -44
- package/src/mcp/transports/stdio.ts +8 -5
- package/src/mcp/types.ts +1 -1
- package/src/modes/components/hook-editor.ts +11 -3
- package/src/modes/components/mcp-add-wizard.ts +6 -2
- package/src/modes/components/model-selector.ts +33 -11
- package/src/modes/controllers/command-controller.ts +6 -4
- package/src/modes/controllers/mcp-command-controller.ts +8 -4
- package/src/prompts/review-custom-request.md +22 -0
- package/src/prompts/review-headless-request.md +16 -0
- package/src/prompts/review-request.md +2 -3
- package/src/prompts/system/project-prompt.md +4 -0
- package/src/prompts/tools/debug.md +1 -0
- package/src/prompts/tools/find.md +4 -2
- package/src/prompts/tools/hashline.md +1 -0
- package/src/sdk.ts +47 -73
- package/src/session/agent-session.ts +93 -27
- package/src/session/streaming-output.ts +1 -1
- package/src/slash-commands/helpers/usage-report.ts +3 -1
- package/src/task/executor.ts +11 -0
- package/src/task/index.ts +19 -0
- package/src/task/render.ts +12 -2
- package/src/task/types.ts +4 -0
- package/src/tools/approval.ts +185 -0
- package/src/tools/ask.ts +1 -0
- package/src/tools/ast-edit.ts +25 -1
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/bash.ts +69 -1
- package/src/tools/browser/tab-supervisor.ts +1 -1
- package/src/tools/browser.ts +15 -0
- package/src/tools/calculator.ts +1 -0
- package/src/tools/checkpoint.ts +2 -0
- package/src/tools/debug.ts +38 -0
- package/src/tools/eval.ts +15 -0
- package/src/tools/find.ts +17 -8
- package/src/tools/gh.ts +21 -1
- package/src/tools/hindsight-recall.ts +1 -0
- package/src/tools/hindsight-reflect.ts +1 -0
- package/src/tools/hindsight-retain.ts +1 -0
- package/src/tools/image-gen.ts +1 -0
- package/src/tools/inspect-image.ts +1 -0
- package/src/tools/irc.ts +1 -0
- package/src/tools/job.ts +1 -0
- package/src/tools/path-utils.ts +14 -1
- package/src/tools/read.ts +1 -0
- package/src/tools/recipe/index.ts +1 -0
- package/src/tools/render-mermaid.ts +1 -0
- package/src/tools/report-tool-issue.ts +1 -0
- package/src/tools/resolve.ts +1 -0
- package/src/tools/review.ts +1 -0
- package/src/tools/search-tool-bm25.ts +1 -0
- package/src/tools/search.ts +1 -0
- package/src/tools/ssh.ts +8 -0
- package/src/tools/todo-write.ts +1 -0
- package/src/tools/write.ts +12 -1
- package/src/tools/yield.ts +1 -0
- package/src/web/search/index.ts +2 -0
package/src/sdk.ts
CHANGED
|
@@ -60,7 +60,6 @@ import {
|
|
|
60
60
|
} from "./extensibility/custom-commands";
|
|
61
61
|
import { discoverAndLoadCustomTools } from "./extensibility/custom-tools";
|
|
62
62
|
import type { CustomTool, CustomToolContext, CustomToolSessionEvent } from "./extensibility/custom-tools/types";
|
|
63
|
-
import { CustomToolAdapter } from "./extensibility/custom-tools/wrapper";
|
|
64
63
|
import {
|
|
65
64
|
discoverAndLoadExtensions,
|
|
66
65
|
type ExtensionContext,
|
|
@@ -343,6 +342,9 @@ export interface CreateAgentSessionOptions {
|
|
|
343
342
|
* `@opentelemetry/api` package returns a no-op tracer in that case.
|
|
344
343
|
*/
|
|
345
344
|
telemetry?: AgentTelemetryConfig;
|
|
345
|
+
|
|
346
|
+
/** Whether to auto-approve all tool calls (--auto-approve CLI flag). Default: false */
|
|
347
|
+
autoApprove?: boolean;
|
|
346
348
|
}
|
|
347
349
|
|
|
348
350
|
/** Result from createAgentSession */
|
|
@@ -835,7 +837,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
835
837
|
// buffer — so we can't rely on it to catch startup events for the extension runner.
|
|
836
838
|
const startupCredentialDisabledEvents: CredentialDisabledEvent[] = [];
|
|
837
839
|
let credentialDisabledTarget: ExtensionRunner | undefined;
|
|
838
|
-
|
|
840
|
+
const unsubscribeCredentialDisabled: (() => void) | undefined = authStorage.onCredentialDisabled(event => {
|
|
839
841
|
if (credentialDisabledTarget) {
|
|
840
842
|
// Discard return: any handler error is routed through runner.onError listeners.
|
|
841
843
|
void credentialDisabledTarget.emitCredentialDisabled(event);
|
|
@@ -1455,29 +1457,25 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1455
1457
|
}
|
|
1456
1458
|
}
|
|
1457
1459
|
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1460
|
+
// The runner is created unconditionally — even with zero extensions loaded — because the
|
|
1461
|
+
// `ExtensionToolWrapper` installed below is the only place the per-tool approval gate runs.
|
|
1462
|
+
// A conditional runner means the approval system silently disappears for users with no
|
|
1463
|
+
// extensions, contradicting non-yolo `tools.approvalMode` settings without feedback.
|
|
1464
|
+
// (Today `createAutoresearchExtension` is unconditionally pushed below, so this scenario
|
|
1465
|
+
// is unreachable; the unconditional construction makes that invariant explicit instead of
|
|
1466
|
+
// implicit, so a future change to make autoresearch optional cannot silently re-open the hole.)
|
|
1467
|
+
const extensionRunner: ExtensionRunner = new ExtensionRunner(
|
|
1468
|
+
extensionsResult.extensions,
|
|
1469
|
+
extensionsResult.runtime,
|
|
1470
|
+
cwd,
|
|
1471
|
+
sessionManager,
|
|
1472
|
+
modelRegistry,
|
|
1473
|
+
);
|
|
1468
1474
|
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
void extensionRunner.emitCredentialDisabled(event);
|
|
1474
|
-
}
|
|
1475
|
-
} else {
|
|
1476
|
-
// No runner to forward to; release our subscription. The embedder's own
|
|
1477
|
-
// onCredentialDisabled (if any) keeps firing through its own subscription.
|
|
1478
|
-
startupCredentialDisabledEvents.length = 0;
|
|
1479
|
-
unsubscribeCredentialDisabled?.();
|
|
1480
|
-
unsubscribeCredentialDisabled = undefined;
|
|
1475
|
+
credentialDisabledTarget = extensionRunner;
|
|
1476
|
+
for (const event of startupCredentialDisabledEvents.splice(0)) {
|
|
1477
|
+
// Discard return: any handler error is routed through runner.onError listeners.
|
|
1478
|
+
void extensionRunner.emitCredentialDisabled(event);
|
|
1481
1479
|
}
|
|
1482
1480
|
|
|
1483
1481
|
const getSessionContext = () => ({
|
|
@@ -1490,38 +1488,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1490
1488
|
session.abort();
|
|
1491
1489
|
},
|
|
1492
1490
|
settings,
|
|
1491
|
+
autoApprove: options.autoApprove ?? false,
|
|
1493
1492
|
});
|
|
1494
1493
|
const toolContextStore = new ToolContextStore(getSessionContext);
|
|
1495
1494
|
|
|
1496
|
-
const registeredTools = extensionRunner
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
return { definition, extensionPath: "<sdk>" };
|
|
1506
|
-
}) ?? []),
|
|
1507
|
-
];
|
|
1508
|
-
wrappedExtensionTools = wrapRegisteredTools(allCustomTools, extensionRunner);
|
|
1509
|
-
} else {
|
|
1510
|
-
// Without extension runner: wrap CustomTools directly with CustomToolAdapter
|
|
1511
|
-
// ToolDefinition items require ExtensionContext and cannot be used without a runner
|
|
1512
|
-
const customToolContext = (): CustomToolContext => ({
|
|
1513
|
-
sessionManager,
|
|
1514
|
-
modelRegistry,
|
|
1515
|
-
model: agent?.state.model,
|
|
1516
|
-
isIdle: () => !session?.isStreaming,
|
|
1517
|
-
hasQueuedMessages: () => (session?.queuedMessageCount ?? 0) > 0,
|
|
1518
|
-
abort: () => session?.abort(),
|
|
1519
|
-
settings,
|
|
1520
|
-
});
|
|
1521
|
-
wrappedExtensionTools = (options.customTools ?? [])
|
|
1522
|
-
.filter(isCustomTool)
|
|
1523
|
-
.map(tool => CustomToolAdapter.wrap(tool, customToolContext));
|
|
1524
|
-
}
|
|
1495
|
+
const registeredTools = extensionRunner.getAllRegisteredTools();
|
|
1496
|
+
const allCustomTools = [
|
|
1497
|
+
...registeredTools,
|
|
1498
|
+
...(options.customTools?.map(tool => {
|
|
1499
|
+
const definition = isCustomTool(tool) ? customToolToDefinition(tool) : tool;
|
|
1500
|
+
return { definition, extensionPath: "<sdk>" };
|
|
1501
|
+
}) ?? []),
|
|
1502
|
+
];
|
|
1503
|
+
const wrappedExtensionTools: Tool[] = wrapRegisteredTools(allCustomTools, extensionRunner);
|
|
1525
1504
|
|
|
1526
1505
|
// All built-in tools are active (conditional tools like git/ask return null from factory if disabled)
|
|
1527
1506
|
const toolRegistry = new Map<string, Tool>();
|
|
@@ -1537,10 +1516,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1537
1516
|
for (const tool of wrappedExtensionTools) {
|
|
1538
1517
|
toolRegistry.set(tool.name, tool);
|
|
1539
1518
|
}
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1519
|
+
// Wrap every tool with `ExtensionToolWrapper` so the per-tool approval gate runs on every
|
|
1520
|
+
// call site, regardless of whether any user extensions are loaded. See the runner-construction
|
|
1521
|
+
// comment above for the safety invariant this enforces.
|
|
1522
|
+
for (const tool of toolRegistry.values()) {
|
|
1523
|
+
toolRegistry.set(tool.name, new ExtensionToolWrapper(tool, extensionRunner));
|
|
1544
1524
|
}
|
|
1545
1525
|
if (model?.provider === "cursor") {
|
|
1546
1526
|
toolRegistry.delete("edit");
|
|
@@ -1564,7 +1544,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1564
1544
|
})) as unknown as AgentTool | null;
|
|
1565
1545
|
if (!sshTool) return null;
|
|
1566
1546
|
const wrapped = wrapToolWithMetaNotice(sshTool);
|
|
1567
|
-
return
|
|
1547
|
+
return new ExtensionToolWrapper(wrapped, extensionRunner) as AgentTool;
|
|
1568
1548
|
};
|
|
1569
1549
|
|
|
1570
1550
|
let cursorEventEmitter: ((event: AgentEvent) => void) | undefined;
|
|
@@ -1824,21 +1804,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1824
1804
|
if (!obfuscator?.hasSecrets()) return converted;
|
|
1825
1805
|
return obfuscateMessages(obfuscator, converted);
|
|
1826
1806
|
};
|
|
1827
|
-
const transformContext =
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
: undefined;
|
|
1837
|
-
const onResponse: SimpleStreamOptions["onResponse"] | undefined = extensionRunner
|
|
1838
|
-
? async (response, model) => {
|
|
1839
|
-
await extensionRunner.emitAfterProviderResponse(response, model);
|
|
1840
|
-
}
|
|
1841
|
-
: undefined;
|
|
1807
|
+
const transformContext = async (messages: AgentMessage[], _signal?: AbortSignal) => {
|
|
1808
|
+
return await extensionRunner.emitContext(messages);
|
|
1809
|
+
};
|
|
1810
|
+
const onPayload = async (payload: unknown, _model?: Model) => {
|
|
1811
|
+
return await extensionRunner.emitBeforeProviderRequest(payload);
|
|
1812
|
+
};
|
|
1813
|
+
const onResponse: SimpleStreamOptions["onResponse"] = async (response, model) => {
|
|
1814
|
+
await extensionRunner.emitAfterProviderResponse(response, model);
|
|
1815
|
+
};
|
|
1842
1816
|
|
|
1843
1817
|
const setToolUIContext = (uiContext: ExtensionUIContext, hasUI: boolean) => {
|
|
1844
1818
|
toolContextStore.setUIContext(uiContext, hasUI);
|
|
@@ -898,7 +898,7 @@ export class AgentSession {
|
|
|
898
898
|
* combined with `Date.now()` so tags stay unique even across rapid
|
|
899
899
|
* same-tick enqueues. */
|
|
900
900
|
#customDisplayTagCounter = 0;
|
|
901
|
-
#postPromptTasks = new Set<Promise<
|
|
901
|
+
#postPromptTasks = new Set<Promise<unknown>>();
|
|
902
902
|
#postPromptTasksPromise: Promise<void> | undefined = undefined;
|
|
903
903
|
#postPromptTasksResolve: (() => void) | undefined = undefined;
|
|
904
904
|
#postPromptTasksAbortController = new AbortController();
|
|
@@ -1786,12 +1786,19 @@ export class AgentSession {
|
|
|
1786
1786
|
|
|
1787
1787
|
const compactionTask = this.#checkCompaction(msg);
|
|
1788
1788
|
this.#trackPostPromptTask(compactionTask);
|
|
1789
|
-
await compactionTask;
|
|
1789
|
+
const compactionDeferredHandoff = await compactionTask;
|
|
1790
1790
|
// Check for incomplete todos only after a final assistant stop, not intermediate tool-use turns.
|
|
1791
1791
|
const hasToolCalls = msg.content.some(content => content.type === "toolCall");
|
|
1792
1792
|
if (hasToolCalls) {
|
|
1793
1793
|
return;
|
|
1794
1794
|
}
|
|
1795
|
+
// When checkCompaction scheduled a deferred handoff, skip the rewind/todo passes:
|
|
1796
|
+
// any reminder we append here would race the handoff's session reset, and
|
|
1797
|
+
// #scheduleAgentContinue would start a fresh streaming turn alongside the handoff
|
|
1798
|
+
// LLM call (visible as "Auto-handoff" loader + an assistant message still streaming).
|
|
1799
|
+
if (compactionDeferredHandoff) {
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1795
1802
|
if (msg.stopReason !== "error" && msg.stopReason !== "aborted") {
|
|
1796
1803
|
if (this.#enforceRewindBeforeYield()) {
|
|
1797
1804
|
return;
|
|
@@ -1840,7 +1847,7 @@ export class AgentSession {
|
|
|
1840
1847
|
this.#postPromptTasksPromise = undefined;
|
|
1841
1848
|
}
|
|
1842
1849
|
|
|
1843
|
-
#trackPostPromptTask(task: Promise<
|
|
1850
|
+
#trackPostPromptTask(task: Promise<unknown>): void {
|
|
1844
1851
|
this.#postPromptTasks.add(task);
|
|
1845
1852
|
this.#ensurePostPromptTasksPromise();
|
|
1846
1853
|
void task
|
|
@@ -1889,8 +1896,17 @@ export class AgentSession {
|
|
|
1889
1896
|
}): void {
|
|
1890
1897
|
this.#schedulePostPromptTask(
|
|
1891
1898
|
async () => {
|
|
1899
|
+
// Defense in depth: if compaction/handoff slipped onto the post-prompt queue
|
|
1900
|
+
// alongside us (e.g. via a scheduler we don't own), refuse to start a fresh
|
|
1901
|
+
// streaming turn — agent.continue() here would race the handoff's session
|
|
1902
|
+
// reset. The first-class fix is in #checkCompaction/the agent_end handler,
|
|
1903
|
+
// but this guard catches anything that bypasses that path.
|
|
1904
|
+
if (this.isCompacting || this.isGeneratingHandoff) {
|
|
1905
|
+
options?.onSkip?.();
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1892
1908
|
if (options?.shouldContinue && !options.shouldContinue()) {
|
|
1893
|
-
options
|
|
1909
|
+
options?.onSkip?.();
|
|
1894
1910
|
return;
|
|
1895
1911
|
}
|
|
1896
1912
|
try {
|
|
@@ -2756,6 +2772,21 @@ export class AgentSession {
|
|
|
2756
2772
|
} catch (error) {
|
|
2757
2773
|
logger.warn("Failed to emit session_shutdown event", { error: String(error) });
|
|
2758
2774
|
}
|
|
2775
|
+
// Abort post-prompt work so the drain below can complete. Without this, a
|
|
2776
|
+
// deferred-handoff task that has already advanced into
|
|
2777
|
+
// `await this.handoff(...) → generateHandoff(...)` keeps awaiting a live LLM stream
|
|
2778
|
+
// — Promise.allSettled() in #cancelPostPromptTasks then waits forever, freezing
|
|
2779
|
+
// /exit and Ctrl+C-double-tap. The post-prompt task's own AbortSignal does not
|
|
2780
|
+
// propagate into the inner handoff/compaction controllers, so we abort them
|
|
2781
|
+
// explicitly. agent.abort() is needed for an agent.continue() that may have
|
|
2782
|
+
// raced the deferred handoff (its streaming loop is awaited by the wrapper IIFE).
|
|
2783
|
+
//
|
|
2784
|
+
// Tool work (bash/eval/python) is NOT aborted here — those have their own
|
|
2785
|
+
// dispose paths and shared kernels are contractually allowed to survive a
|
|
2786
|
+
// session's dispose.
|
|
2787
|
+
this.abortRetry();
|
|
2788
|
+
this.abortCompaction();
|
|
2789
|
+
this.agent.abort();
|
|
2759
2790
|
await this.#cancelPostPromptTasks();
|
|
2760
2791
|
// Cancel jobs this agent registered so a subagent's teardown doesn't
|
|
2761
2792
|
// leak its background bash/task work into the parent's manager. Only
|
|
@@ -4050,10 +4081,13 @@ export class AgentSession {
|
|
|
4050
4081
|
);
|
|
4051
4082
|
}
|
|
4052
4083
|
|
|
4053
|
-
// Check if we need to compact before sending (catches aborted responses)
|
|
4084
|
+
// Check if we need to compact before sending (catches aborted responses). Run
|
|
4085
|
+
// inline (allowDefer=false) so the handoff/maintenance fully settles before this
|
|
4086
|
+
// prompt's agent loop starts — otherwise a deferred handoff would fire on the
|
|
4087
|
+
// next microtask alongside the new turn.
|
|
4054
4088
|
const lastAssistant = this.#findLastAssistantMessage();
|
|
4055
4089
|
if (lastAssistant && !options?.skipCompactionCheck) {
|
|
4056
|
-
await this.#checkCompaction(lastAssistant, false);
|
|
4090
|
+
await this.#checkCompaction(lastAssistant, false, false);
|
|
4057
4091
|
}
|
|
4058
4092
|
|
|
4059
4093
|
// Build messages array (session context, eager todo prelude, then active prompt message)
|
|
@@ -5602,10 +5636,23 @@ export class AgentSession {
|
|
|
5602
5636
|
*
|
|
5603
5637
|
* @param assistantMessage The assistant message to check
|
|
5604
5638
|
* @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
|
|
5639
|
+
* @param allowDefer If true, threshold-driven handoff strategy may schedule itself as a
|
|
5640
|
+
* deferred post-prompt task instead of running inline. Callers running inside the
|
|
5641
|
+
* `agent_end` handler set this to true so `session.prompt()` resolves cleanly; callers
|
|
5642
|
+
* on the pre-prompt path (where the next agent turn is about to start) set it to false
|
|
5643
|
+
* to avoid racing the deferred handoff against the new turn.
|
|
5644
|
+
* @returns true when a deferred handoff was scheduled. Callers MUST then skip any
|
|
5645
|
+
* subsequent `#scheduleAgentContinue` / reminder appends for this turn — the
|
|
5646
|
+
* handoff will replace session state and a concurrent `agent.continue()` would
|
|
5647
|
+
* stream into the soon-to-be-discarded session.
|
|
5605
5648
|
*/
|
|
5606
|
-
async #checkCompaction(
|
|
5649
|
+
async #checkCompaction(
|
|
5650
|
+
assistantMessage: AssistantMessage,
|
|
5651
|
+
skipAbortedCheck = true,
|
|
5652
|
+
allowDefer = true,
|
|
5653
|
+
): Promise<boolean> {
|
|
5607
5654
|
// Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
|
|
5608
|
-
if (skipAbortedCheck && assistantMessage.stopReason === "aborted") return;
|
|
5655
|
+
if (skipAbortedCheck && assistantMessage.stopReason === "aborted") return false;
|
|
5609
5656
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
5610
5657
|
const generation = this.#promptGeneration;
|
|
5611
5658
|
// Skip overflow check if the message came from a different model.
|
|
@@ -5634,22 +5681,22 @@ export class AgentSession {
|
|
|
5634
5681
|
if (promoted) {
|
|
5635
5682
|
// Retry on the promoted (larger) model without compacting
|
|
5636
5683
|
this.#scheduleAgentContinue({ delayMs: 100, generation });
|
|
5637
|
-
return;
|
|
5684
|
+
return false;
|
|
5638
5685
|
}
|
|
5639
5686
|
|
|
5640
5687
|
// No promotion target available fall through to compaction
|
|
5641
5688
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
5642
5689
|
if (compactionSettings.enabled && compactionSettings.strategy !== "off") {
|
|
5643
|
-
await this.#runAutoCompaction("overflow", true);
|
|
5690
|
+
await this.#runAutoCompaction("overflow", true, false, allowDefer);
|
|
5644
5691
|
}
|
|
5645
|
-
return;
|
|
5692
|
+
return false;
|
|
5646
5693
|
}
|
|
5647
5694
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
5648
|
-
if (!compactionSettings.enabled || compactionSettings.strategy === "off") return;
|
|
5695
|
+
if (!compactionSettings.enabled || compactionSettings.strategy === "off") return false;
|
|
5649
5696
|
|
|
5650
5697
|
// Case 2: Threshold - turn succeeded but context is getting large
|
|
5651
5698
|
// Skip if this was an error (non-overflow errors don't have usage data)
|
|
5652
|
-
if (assistantMessage.stopReason === "error") return;
|
|
5699
|
+
if (assistantMessage.stopReason === "error") return false;
|
|
5653
5700
|
const pruneResult = await this.#pruneToolOutputs();
|
|
5654
5701
|
let contextTokens = calculateContextTokens(assistantMessage.usage);
|
|
5655
5702
|
if (pruneResult) {
|
|
@@ -5659,9 +5706,10 @@ export class AgentSession {
|
|
|
5659
5706
|
// Try promotion first — if a larger model is available, switch instead of compacting
|
|
5660
5707
|
const promoted = await this.#tryContextPromotion(assistantMessage);
|
|
5661
5708
|
if (!promoted) {
|
|
5662
|
-
await this.#runAutoCompaction("threshold", false);
|
|
5709
|
+
return await this.#runAutoCompaction("threshold", false, false, allowDefer);
|
|
5663
5710
|
}
|
|
5664
5711
|
}
|
|
5712
|
+
return false;
|
|
5665
5713
|
}
|
|
5666
5714
|
#assistantEndedWithSuccessfulYield(assistantMessage: AssistantMessage): boolean {
|
|
5667
5715
|
const toolCallId = this.#lastSuccessfulYieldToolCallId;
|
|
@@ -6352,17 +6400,34 @@ export class AgentSession {
|
|
|
6352
6400
|
|
|
6353
6401
|
/**
|
|
6354
6402
|
* Internal: Run auto-compaction with events.
|
|
6403
|
+
*
|
|
6404
|
+
* @param allowDefer If true (default), threshold-driven handoff strategy is allowed to
|
|
6405
|
+
* schedule itself as a deferred post-prompt task and return `true` immediately. The
|
|
6406
|
+
* caller MUST treat that as "compaction will happen async — do not also schedule
|
|
6407
|
+
* `agent.continue()` for this turn", otherwise the deferred handoff races a fresh
|
|
6408
|
+
* streaming turn (the symptom: "Auto-handoff" loader + assistant message still
|
|
6409
|
+
* streaming). Callers on a path that is about to start a new agent turn (e.g.
|
|
6410
|
+
* the pre-prompt check in `#promptWithMessage`) pass `false` to force inline
|
|
6411
|
+
* execution so the handoff completes before the new turn begins.
|
|
6412
|
+
* @returns true when a deferred handoff was scheduled. Inline runs always return false.
|
|
6355
6413
|
*/
|
|
6356
6414
|
async #runAutoCompaction(
|
|
6357
6415
|
reason: "overflow" | "threshold" | "idle",
|
|
6358
6416
|
willRetry: boolean,
|
|
6359
6417
|
deferred = false,
|
|
6360
|
-
|
|
6418
|
+
allowDefer = true,
|
|
6419
|
+
): Promise<boolean> {
|
|
6361
6420
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
6362
|
-
if (compactionSettings.strategy === "off") return;
|
|
6363
|
-
if (reason !== "idle" && !compactionSettings.enabled) return;
|
|
6421
|
+
if (compactionSettings.strategy === "off") return false;
|
|
6422
|
+
if (reason !== "idle" && !compactionSettings.enabled) return false;
|
|
6364
6423
|
const generation = this.#promptGeneration;
|
|
6365
|
-
if (
|
|
6424
|
+
if (
|
|
6425
|
+
!deferred &&
|
|
6426
|
+
allowDefer &&
|
|
6427
|
+
reason !== "overflow" &&
|
|
6428
|
+
reason !== "idle" &&
|
|
6429
|
+
compactionSettings.strategy === "handoff"
|
|
6430
|
+
) {
|
|
6366
6431
|
this.#schedulePostPromptTask(
|
|
6367
6432
|
async signal => {
|
|
6368
6433
|
await Promise.resolve();
|
|
@@ -6371,7 +6436,7 @@ export class AgentSession {
|
|
|
6371
6436
|
},
|
|
6372
6437
|
{ generation },
|
|
6373
6438
|
);
|
|
6374
|
-
return;
|
|
6439
|
+
return true;
|
|
6375
6440
|
}
|
|
6376
6441
|
|
|
6377
6442
|
let action: "context-full" | "handoff" =
|
|
@@ -6400,7 +6465,7 @@ export class AgentSession {
|
|
|
6400
6465
|
aborted: true,
|
|
6401
6466
|
willRetry: false,
|
|
6402
6467
|
});
|
|
6403
|
-
return;
|
|
6468
|
+
return false;
|
|
6404
6469
|
}
|
|
6405
6470
|
logger.warn("Auto-handoff returned no document; falling back to context-full maintenance", {
|
|
6406
6471
|
reason,
|
|
@@ -6418,7 +6483,7 @@ export class AgentSession {
|
|
|
6418
6483
|
if (!autoCompactionSignal.aborted && reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
6419
6484
|
this.#scheduleAutoContinuePrompt(generation);
|
|
6420
6485
|
}
|
|
6421
|
-
return;
|
|
6486
|
+
return false;
|
|
6422
6487
|
}
|
|
6423
6488
|
}
|
|
6424
6489
|
|
|
@@ -6431,7 +6496,7 @@ export class AgentSession {
|
|
|
6431
6496
|
willRetry: false,
|
|
6432
6497
|
skipped: true,
|
|
6433
6498
|
});
|
|
6434
|
-
return;
|
|
6499
|
+
return false;
|
|
6435
6500
|
}
|
|
6436
6501
|
|
|
6437
6502
|
const availableModels = this.#modelRegistry.getAvailable();
|
|
@@ -6444,7 +6509,7 @@ export class AgentSession {
|
|
|
6444
6509
|
willRetry: false,
|
|
6445
6510
|
skipped: true,
|
|
6446
6511
|
});
|
|
6447
|
-
return;
|
|
6512
|
+
return false;
|
|
6448
6513
|
}
|
|
6449
6514
|
|
|
6450
6515
|
const pathEntries = this.sessionManager.getBranch();
|
|
@@ -6466,7 +6531,7 @@ export class AgentSession {
|
|
|
6466
6531
|
shouldContinue: () => this.agent.hasQueuedMessages(),
|
|
6467
6532
|
});
|
|
6468
6533
|
}
|
|
6469
|
-
return;
|
|
6534
|
+
return false;
|
|
6470
6535
|
}
|
|
6471
6536
|
|
|
6472
6537
|
let hookCompaction: CompactionResult | undefined;
|
|
@@ -6490,7 +6555,7 @@ export class AgentSession {
|
|
|
6490
6555
|
aborted: true,
|
|
6491
6556
|
willRetry: false,
|
|
6492
6557
|
});
|
|
6493
|
-
return;
|
|
6558
|
+
return false;
|
|
6494
6559
|
}
|
|
6495
6560
|
|
|
6496
6561
|
if (hookResult?.compaction) {
|
|
@@ -6621,7 +6686,7 @@ export class AgentSession {
|
|
|
6621
6686
|
aborted: true,
|
|
6622
6687
|
willRetry: false,
|
|
6623
6688
|
});
|
|
6624
|
-
return;
|
|
6689
|
+
return false;
|
|
6625
6690
|
}
|
|
6626
6691
|
|
|
6627
6692
|
this.sessionManager.appendCompaction(
|
|
@@ -6692,7 +6757,7 @@ export class AgentSession {
|
|
|
6692
6757
|
aborted: true,
|
|
6693
6758
|
willRetry: false,
|
|
6694
6759
|
});
|
|
6695
|
-
return;
|
|
6760
|
+
return false;
|
|
6696
6761
|
}
|
|
6697
6762
|
const errorMessage = error instanceof Error ? error.message : "compaction failed";
|
|
6698
6763
|
await this.#emitSessionEvent({
|
|
@@ -6711,6 +6776,7 @@ export class AgentSession {
|
|
|
6711
6776
|
this.#autoCompactionAbortController = undefined;
|
|
6712
6777
|
}
|
|
6713
6778
|
}
|
|
6779
|
+
return false;
|
|
6714
6780
|
}
|
|
6715
6781
|
|
|
6716
6782
|
/**
|
|
@@ -9,7 +9,7 @@ import { sanitizeWithOptionalSixelPassthrough } from "../utils/sixel";
|
|
|
9
9
|
|
|
10
10
|
export const DEFAULT_MAX_LINES = 3000;
|
|
11
11
|
export const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB
|
|
12
|
-
export const DEFAULT_MAX_COLUMN =
|
|
12
|
+
export const DEFAULT_MAX_COLUMN = 512; // Max chars per grep match line
|
|
13
13
|
|
|
14
14
|
const NL = "\n";
|
|
15
15
|
const ELLIPSIS = "…";
|
|
@@ -56,7 +56,9 @@ function renderUsageReports(reports: UsageReport[], nowMs: number): string {
|
|
|
56
56
|
lines.push(`- ${limit.label}${tier}${window ? ` — ${window}` : ""}`);
|
|
57
57
|
lines.push(` ${formatUsageReportAccount(report, limit, index)}: ${formatUsageAmount(limit)}`);
|
|
58
58
|
lines.push(` ${renderAsciiBar(limit.amount.usedFraction)}`);
|
|
59
|
-
if (limit.window?.resetsAt
|
|
59
|
+
if (limit.window?.resetsAt && limit.window.resetsAt > nowMs) {
|
|
60
|
+
lines.push(` resets in ${formatDuration(limit.window.resetsAt - nowMs)}`);
|
|
61
|
+
}
|
|
60
62
|
if (limit.notes && limit.notes.length > 0) lines.push(` ${limit.notes.join(" • ")}`);
|
|
61
63
|
}
|
|
62
64
|
}
|
package/src/task/executor.ts
CHANGED
|
@@ -532,6 +532,11 @@ function createSubagentSettings(baseSettings: Settings): Settings {
|
|
|
532
532
|
...snapshot,
|
|
533
533
|
"async.enabled": false,
|
|
534
534
|
"bash.autoBackground.enabled": false,
|
|
535
|
+
// Subagents run headless — there is no UI to confirm prompts against, so
|
|
536
|
+
// the parent task approval is the authorization boundary. Use yolo mode
|
|
537
|
+
// to preserve unattended subagent execution while still honoring any
|
|
538
|
+
// tool-level safety override that can be handled before execution.
|
|
539
|
+
"tools.approvalMode": "yolo",
|
|
535
540
|
});
|
|
536
541
|
}
|
|
537
542
|
|
|
@@ -1148,6 +1153,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1148
1153
|
if (model?.contextWindow && model.contextWindow > 0) {
|
|
1149
1154
|
progress.contextWindow = model.contextWindow;
|
|
1150
1155
|
}
|
|
1156
|
+
if (model) {
|
|
1157
|
+
progress.resolvedModel = explicitThinkingLevel
|
|
1158
|
+
? `${model.provider}/${model.id}:${resolvedThinkingLevel}`
|
|
1159
|
+
: `${model.provider}/${model.id}`;
|
|
1160
|
+
}
|
|
1151
1161
|
const effectiveThinkingLevel = explicitThinkingLevel
|
|
1152
1162
|
? resolvedThinkingLevel
|
|
1153
1163
|
: (thinkingLevel ?? resolvedThinkingLevel);
|
|
@@ -1606,6 +1616,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1606
1616
|
contextTokens: progress.contextTokens,
|
|
1607
1617
|
contextWindow: progress.contextWindow,
|
|
1608
1618
|
modelOverride,
|
|
1619
|
+
resolvedModel: progress.resolvedModel,
|
|
1609
1620
|
error: exitCode !== 0 && stderr ? stderr : undefined,
|
|
1610
1621
|
aborted: wasAborted,
|
|
1611
1622
|
abortReason: finalAbortReason,
|
package/src/task/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ import planModeSubagentPrompt from "../prompts/system/plan-mode-subagent.md" wit
|
|
|
27
27
|
import subagentUserPromptTemplate from "../prompts/system/subagent-user-prompt.md" with { type: "text" };
|
|
28
28
|
import taskDescriptionTemplate from "../prompts/tools/task.md" with { type: "text" };
|
|
29
29
|
import taskSummaryTemplate from "../prompts/tools/task-summary.md" with { type: "text" };
|
|
30
|
+
import { truncateForPrompt } from "../tools/approval";
|
|
30
31
|
import { formatBytes, formatDuration } from "../tools/render-utils";
|
|
31
32
|
import {
|
|
32
33
|
type AgentDefinition,
|
|
@@ -214,6 +215,24 @@ function validateTaskModeParams(simpleMode: TaskSimpleMode, params: TaskParams):
|
|
|
214
215
|
*/
|
|
215
216
|
export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetails, Theme> {
|
|
216
217
|
readonly name = "task";
|
|
218
|
+
readonly approval = "exec" as const;
|
|
219
|
+
readonly formatApprovalDetails = (args: unknown): string[] => {
|
|
220
|
+
const params = args as Partial<TaskParams>;
|
|
221
|
+
const lines: string[] = [];
|
|
222
|
+
if (typeof params.agent === "string") {
|
|
223
|
+
lines.push(`Agent: ${truncateForPrompt(params.agent)}`);
|
|
224
|
+
}
|
|
225
|
+
const tasks = Array.isArray(params.tasks) ? params.tasks : [];
|
|
226
|
+
const firstTask = tasks[0];
|
|
227
|
+
if (firstTask) {
|
|
228
|
+
lines.push(`Task: ${truncateForPrompt(firstTask.id)}`);
|
|
229
|
+
lines.push(`Assignment:\n${truncateForPrompt(firstTask.assignment)}`);
|
|
230
|
+
if (tasks.length > 1) {
|
|
231
|
+
lines.push(`+${tasks.length - 1} more task${tasks.length === 2 ? "" : "s"}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return lines;
|
|
235
|
+
};
|
|
217
236
|
readonly label = "Task";
|
|
218
237
|
readonly summary = "Spawn a subagent to complete a parallel task";
|
|
219
238
|
readonly strict = true;
|
package/src/task/render.ts
CHANGED
|
@@ -8,6 +8,7 @@ import path from "node:path";
|
|
|
8
8
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
9
9
|
import { Container, Text } from "@oh-my-pi/pi-tui";
|
|
10
10
|
import { formatNumber } from "@oh-my-pi/pi-utils";
|
|
11
|
+
import { settings } from "../config/settings";
|
|
11
12
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
12
13
|
import type { Theme } from "../modes/theme/theme";
|
|
13
14
|
import {
|
|
@@ -59,6 +60,8 @@ function appendAgentStats(
|
|
|
59
60
|
contextTokens?: number;
|
|
60
61
|
contextWindow?: number;
|
|
61
62
|
cost: number;
|
|
63
|
+
resolvedModel?: string;
|
|
64
|
+
showResolvedModelBadge?: boolean;
|
|
62
65
|
},
|
|
63
66
|
theme: Theme,
|
|
64
67
|
): string {
|
|
@@ -83,6 +86,9 @@ function appendAgentStats(
|
|
|
83
86
|
if (opts.cost > 0) {
|
|
84
87
|
line += `${theme.sep.dot}${theme.fg("statusLineCost", `$${opts.cost.toFixed(2)}`)}`;
|
|
85
88
|
}
|
|
89
|
+
if (opts.resolvedModel && opts.showResolvedModelBadge) {
|
|
90
|
+
line += `${theme.sep.dot}${theme.fg("dim", truncateToWidth(replaceTabs(opts.resolvedModel), 30))}`;
|
|
91
|
+
}
|
|
86
92
|
return line;
|
|
87
93
|
}
|
|
88
94
|
|
|
@@ -564,14 +570,15 @@ function renderAgentProgress(
|
|
|
564
570
|
statusLine += ` ${formatBadge(statusLabel, iconColor, theme)}`;
|
|
565
571
|
}
|
|
566
572
|
|
|
573
|
+
const showBadge = settings.get("task.showResolvedModelBadge");
|
|
567
574
|
if (progress.status === "running") {
|
|
568
575
|
if (!description) {
|
|
569
576
|
const taskPreview = truncateToWidth(progress.assignment ?? progress.task, 40);
|
|
570
577
|
statusLine += ` ${theme.fg("muted", taskPreview)}`;
|
|
571
578
|
}
|
|
572
|
-
statusLine = appendAgentStats(statusLine, progress, theme);
|
|
579
|
+
statusLine = appendAgentStats(statusLine, { ...progress, showResolvedModelBadge: showBadge }, theme);
|
|
573
580
|
} else if (progress.status === "completed") {
|
|
574
|
-
statusLine = appendAgentStats(statusLine, progress, theme);
|
|
581
|
+
statusLine = appendAgentStats(statusLine, { ...progress, showResolvedModelBadge: showBadge }, theme);
|
|
575
582
|
}
|
|
576
583
|
|
|
577
584
|
lines.push(statusLine);
|
|
@@ -838,6 +845,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
838
845
|
iconColor,
|
|
839
846
|
theme,
|
|
840
847
|
)}`;
|
|
848
|
+
const showBadge = settings.get("task.showResolvedModelBadge");
|
|
841
849
|
statusLine = appendAgentStats(
|
|
842
850
|
statusLine,
|
|
843
851
|
{
|
|
@@ -845,6 +853,8 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
845
853
|
contextTokens: result.contextTokens,
|
|
846
854
|
contextWindow: result.contextWindow,
|
|
847
855
|
cost: result.usage?.cost.total ?? 0,
|
|
856
|
+
resolvedModel: result.resolvedModel,
|
|
857
|
+
showResolvedModelBadge: showBadge,
|
|
848
858
|
},
|
|
849
859
|
theme,
|
|
850
860
|
);
|
package/src/task/types.ts
CHANGED
|
@@ -210,6 +210,8 @@ export interface AgentProgress {
|
|
|
210
210
|
cost: number;
|
|
211
211
|
durationMs: number;
|
|
212
212
|
modelOverride?: string | string[];
|
|
213
|
+
/** Resolved model display string in the form `<provider>/<id>`, optionally suffixed with `:<thinkingLevel>` when the level was set explicitly. Undefined when the model could not be resolved. */
|
|
214
|
+
resolvedModel?: string;
|
|
213
215
|
/** Data extracted by registered subprocess tool handlers (keyed by tool name) */
|
|
214
216
|
extractedToolData?: Record<string, unknown[]>;
|
|
215
217
|
/**
|
|
@@ -268,6 +270,8 @@ export interface SingleResult {
|
|
|
268
270
|
/** Model's context window in tokens, when known. */
|
|
269
271
|
contextWindow?: number;
|
|
270
272
|
modelOverride?: string | string[];
|
|
273
|
+
/** Resolved model display string in the form `<provider>/<id>`, optionally suffixed with `:<thinkingLevel>` when the level was set explicitly. Omitted from tool-result JSON when undefined to keep wire payloads small. */
|
|
274
|
+
resolvedModel?: string;
|
|
271
275
|
error?: string;
|
|
272
276
|
aborted?: boolean;
|
|
273
277
|
abortReason?: string;
|