@oh-my-pi/pi-coding-agent 15.10.12 → 15.11.1
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 +90 -4
- package/dist/cli.js +869 -825
- package/dist/types/async/index.d.ts +0 -1
- package/dist/types/capability/mcp.d.ts +1 -0
- package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
- package/dist/types/config/keybindings.d.ts +6 -1
- package/dist/types/config/settings-schema.d.ts +66 -34
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/history-protocol.d.ts +14 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/irc/bus.d.ts +66 -0
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/oauth-flow.d.ts +6 -1
- package/dist/types/mcp/transports/stdio.d.ts +1 -0
- package/dist/types/mcp/types.d.ts +2 -0
- package/dist/types/modes/components/agent-hub.d.ts +30 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -0
- package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
- package/dist/types/modes/components/custom-editor.d.ts +2 -0
- package/dist/types/modes/components/mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +3 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/transcript-container.d.ts +3 -2
- package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
- package/dist/types/modes/components/welcome.d.ts +3 -9
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +43 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -2
- package/dist/types/modes/theme/theme.d.ts +3 -1
- package/dist/types/modes/types.d.ts +3 -2
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
- package/dist/types/registry/agent-lifecycle.d.ts +51 -0
- package/dist/types/registry/agent-registry.d.ts +16 -5
- package/dist/types/session/agent-session.d.ts +35 -30
- package/dist/types/session/messages.d.ts +2 -4
- package/dist/types/session/session-history-format.d.ts +12 -0
- package/dist/types/session/session-manager.d.ts +21 -3
- package/dist/types/session/streaming-output.d.ts +23 -0
- package/dist/types/task/executor.d.ts +11 -2
- package/dist/types/task/index.d.ts +11 -4
- package/dist/types/task/output-manager.d.ts +0 -7
- package/dist/types/task/repair-args.d.ts +8 -7
- package/dist/types/task/types.d.ts +55 -51
- package/dist/types/tools/browser/tab-worker.d.ts +3 -1
- package/dist/types/tools/find.d.ts +0 -11
- package/dist/types/tools/grouped-file-output.d.ts +0 -49
- package/dist/types/tools/index.d.ts +1 -3
- package/dist/types/tools/irc.d.ts +76 -38
- package/dist/types/tools/job.d.ts +7 -1
- package/dist/types/tools/render-utils.d.ts +22 -0
- package/examples/extensions/with-deps/package.json +1 -0
- package/package.json +11 -10
- package/scripts/bundle-dist.ts +28 -19
- package/src/async/index.ts +0 -1
- package/src/capability/mcp.ts +1 -0
- package/src/cli/gallery-cli.ts +6 -5
- package/src/cli/gallery-fixtures/agentic.ts +230 -115
- package/src/cli/gallery-fixtures/types.ts +5 -0
- package/src/cli.ts +20 -6
- package/src/commit/agentic/tools/analyze-file.ts +38 -19
- package/src/config/keybindings.ts +6 -1
- package/src/config/mcp-schema.json +4 -0
- package/src/config/settings-schema.ts +68 -41
- package/src/config/settings.ts +7 -0
- package/src/edit/renderer.ts +96 -46
- package/src/eval/__tests__/agent-bridge.test.ts +5 -3
- package/src/eval/agent-bridge.ts +3 -16
- package/src/eval/js/shared/prelude.txt +1 -1
- package/src/eval/py/prelude.py +5 -6
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +44 -14
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/shared-events.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +9 -9
- package/src/internal-urls/history-protocol.ts +113 -0
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/router.ts +3 -1
- package/src/internal-urls/types.ts +1 -1
- package/src/irc/bus.ts +292 -0
- package/src/main.ts +8 -60
- package/src/mcp/manager.ts +3 -0
- package/src/mcp/oauth-discovery.ts +27 -2
- package/src/mcp/oauth-flow.ts +47 -1
- package/src/mcp/transports/stdio.ts +3 -0
- package/src/mcp/types.ts +2 -0
- package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
- package/src/modes/components/assistant-message.ts +15 -0
- package/src/modes/components/btw-panel.ts +5 -1
- package/src/modes/components/compaction-summary-message.ts +68 -32
- package/src/modes/components/custom-editor.ts +10 -0
- package/src/modes/components/mcp-add-wizard.ts +13 -0
- package/src/modes/components/settings-selector.ts +2 -0
- package/src/modes/components/status-line/component.ts +22 -12
- package/src/modes/components/status-line/types.ts +3 -0
- package/src/modes/components/tool-execution.ts +31 -1
- package/src/modes/components/transcript-container.ts +99 -18
- package/src/modes/components/tree-selector.ts +6 -1
- package/src/modes/components/ttsr-notification.ts +72 -30
- package/src/modes/components/welcome.ts +9 -33
- package/src/modes/controllers/event-controller.ts +93 -4
- package/src/modes/controllers/extension-ui-controller.ts +8 -8
- package/src/modes/controllers/input-controller.ts +18 -2
- package/src/modes/controllers/mcp-command-controller.ts +34 -2
- package/src/modes/controllers/selector-controller.ts +25 -17
- package/src/modes/controllers/tool-args-reveal.ts +174 -0
- package/src/modes/interactive-mode.ts +17 -15
- package/src/modes/theme/theme.ts +24 -5
- package/src/modes/types.ts +3 -5
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +51 -49
- package/src/prompts/system/irc-incoming.md +3 -4
- package/src/prompts/system/orchestrate-notice.md +2 -2
- package/src/prompts/system/subagent-system-prompt.md +0 -5
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/system/workflow-notice.md +2 -2
- package/src/prompts/tools/eval.md +3 -3
- package/src/prompts/tools/irc.md +29 -19
- package/src/prompts/tools/read.md +2 -2
- package/src/prompts/tools/task-summary.md +5 -16
- package/src/prompts/tools/task.md +43 -29
- package/src/registry/agent-lifecycle.ts +218 -0
- package/src/registry/agent-registry.ts +16 -5
- package/src/sdk.ts +29 -9
- package/src/session/agent-session.ts +268 -241
- package/src/session/messages.ts +11 -78
- package/src/session/session-history-format.ts +246 -0
- package/src/session/session-manager.ts +59 -5
- package/src/session/streaming-output.ts +60 -0
- package/src/task/executor.ts +855 -466
- package/src/task/index.ts +723 -794
- package/src/task/output-manager.ts +0 -11
- package/src/task/render.ts +142 -66
- package/src/task/repair-args.ts +21 -9
- package/src/task/types.ts +73 -66
- package/src/tools/ask.ts +4 -2
- package/src/tools/bash.ts +15 -5
- package/src/tools/browser/tab-worker.ts +26 -7
- package/src/tools/browser.ts +28 -1
- package/src/tools/find.ts +2 -27
- package/src/tools/grouped-file-output.ts +1 -118
- package/src/tools/index.ts +4 -12
- package/src/tools/irc.ts +596 -171
- package/src/tools/job.ts +41 -7
- package/src/tools/read.ts +57 -1
- package/src/tools/render-utils.ts +56 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +4 -1
- package/src/tools/write.ts +65 -47
- package/src/web/search/providers/anthropic.ts +29 -4
- package/dist/types/async/support.d.ts +0 -2
- package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
- package/dist/types/task/simple-mode.d.ts +0 -8
- package/src/async/support.ts +0 -5
- package/src/task/simple-mode.ts +0 -27
|
@@ -85,15 +85,4 @@ export class AgentOutputManager {
|
|
|
85
85
|
await this.#ensureInitialized();
|
|
86
86
|
return this.#allocateUnique(id);
|
|
87
87
|
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Allocate unique IDs for a batch of tasks.
|
|
91
|
-
*
|
|
92
|
-
* @param ids Array of requested IDs
|
|
93
|
-
* @returns Array of unique IDs in same order
|
|
94
|
-
*/
|
|
95
|
-
async allocateBatch(ids: string[]): Promise<string[]> {
|
|
96
|
-
await this.#ensureInitialized();
|
|
97
|
-
return ids.map(id => this.#allocateUnique(id));
|
|
98
|
-
}
|
|
99
88
|
}
|
package/src/task/render.ts
CHANGED
|
@@ -62,6 +62,7 @@ function appendAgentStats(
|
|
|
62
62
|
line: string,
|
|
63
63
|
opts: {
|
|
64
64
|
toolCount?: number;
|
|
65
|
+
requests?: number;
|
|
65
66
|
tokens: number;
|
|
66
67
|
contextTokens?: number;
|
|
67
68
|
contextWindow?: number;
|
|
@@ -74,6 +75,9 @@ function appendAgentStats(
|
|
|
74
75
|
if (opts.toolCount) {
|
|
75
76
|
line += `${theme.sep.dot}${theme.fg("dim", `${formatNumber(opts.toolCount)} ${theme.icon.extensionTool}`)}`;
|
|
76
77
|
}
|
|
78
|
+
if (opts.requests) {
|
|
79
|
+
line += `${theme.sep.dot}${theme.fg("dim", `${formatNumber(opts.requests)} req`)}`;
|
|
80
|
+
}
|
|
77
81
|
// Current per-turn context — match the status line's `<pct>%/<window>` gauge (e.g. `5.1%/1M`).
|
|
78
82
|
if (opts.contextTokens && opts.contextTokens > 0) {
|
|
79
83
|
const ctx =
|
|
@@ -505,65 +509,106 @@ function formatOutputInline(data: unknown, theme: Theme, maxWidth = 80): string
|
|
|
505
509
|
}
|
|
506
510
|
|
|
507
511
|
/**
|
|
508
|
-
* Render the
|
|
509
|
-
*
|
|
510
|
-
* trailing entries may be partially parsed — every field access is defensive.
|
|
512
|
+
* Render the call preview lines for the single spawned agent. The
|
|
513
|
+
* args stream in token by token, so every field access is defensive.
|
|
511
514
|
*/
|
|
512
|
-
function
|
|
513
|
-
|
|
514
|
-
if (items.length === 0) return [];
|
|
515
|
-
|
|
515
|
+
function renderTaskCallLines(args: Partial<TaskParams> | undefined, theme: Theme): string[] {
|
|
516
|
+
if (!args) return [];
|
|
516
517
|
const bullet = theme.fg("dim", "•");
|
|
517
|
-
const
|
|
518
|
-
|
|
518
|
+
const lines: string[] = [];
|
|
519
|
+
|
|
520
|
+
const rawId = typeof args.id === "string" ? args.id.trim() : "";
|
|
521
|
+
const idLabel = rawId ? formatTaskId(rawId) : "";
|
|
522
|
+
const desc = typeof args.description === "string" ? args.description.trim() : "";
|
|
523
|
+
if (idLabel || desc) {
|
|
524
|
+
let line = `${bullet} ${theme.fg("accent", theme.bold(idLabel || "agent"))}`;
|
|
525
|
+
if (desc) {
|
|
526
|
+
line += `: ${theme.fg("muted", truncateToWidth(replaceTabs(desc), 64))}`;
|
|
527
|
+
}
|
|
528
|
+
lines.push(line);
|
|
529
|
+
}
|
|
530
|
+
lines.push(...renderTaskItemLines(args.tasks, theme));
|
|
531
|
+
return lines;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Render the per-item list (`id` + ui `description`) for a batch call's
|
|
536
|
+
* streaming preview. The args stream in token by token, so the array grows
|
|
537
|
+
* over time and trailing entries may be partially parsed — every field access
|
|
538
|
+
* is defensive.
|
|
539
|
+
*/
|
|
540
|
+
function renderTaskItemLines(tasks: TaskItem[] | undefined, theme: Theme): string[] {
|
|
541
|
+
if (!Array.isArray(tasks) || tasks.length === 0) return [];
|
|
519
542
|
|
|
543
|
+
const bullet = theme.fg("dim", "•");
|
|
544
|
+
const cap = Math.min(tasks.length, 12);
|
|
520
545
|
const lines: string[] = [];
|
|
521
546
|
for (let i = 0; i < cap; i++) {
|
|
522
|
-
const task =
|
|
523
|
-
const rawId = task?.id
|
|
547
|
+
const task = tasks[i] as Partial<TaskItem> | undefined;
|
|
548
|
+
const rawId = typeof task?.id === "string" ? task.id.trim() : "";
|
|
524
549
|
const idLabel = rawId ? formatTaskId(rawId) : `#${i + 1}`;
|
|
525
550
|
let line = `${bullet} ${theme.fg("accent", theme.bold(idLabel))}`;
|
|
526
|
-
const desc = task?.description
|
|
551
|
+
const desc = typeof task?.description === "string" ? task.description.trim() : "";
|
|
527
552
|
if (desc) {
|
|
528
553
|
line += `: ${theme.fg("muted", truncateToWidth(replaceTabs(desc), 64))}`;
|
|
529
554
|
}
|
|
555
|
+
if (task?.isolated === true) {
|
|
556
|
+
line += theme.fg("dim", " [isolated]");
|
|
557
|
+
}
|
|
530
558
|
lines.push(line);
|
|
531
559
|
}
|
|
532
|
-
if (
|
|
533
|
-
lines.push(`${bullet} ${theme.fg("dim", formatMoreItems(
|
|
560
|
+
if (cap < tasks.length) {
|
|
561
|
+
lines.push(`${bullet} ${theme.fg("dim", formatMoreItems(tasks.length - cap, "agent"))}`);
|
|
534
562
|
}
|
|
535
563
|
return lines;
|
|
536
564
|
}
|
|
537
565
|
|
|
538
|
-
/**
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
* the merged result frame so the brief stays visible for the whole task
|
|
542
|
-
* lifecycle — not just until the first progress snapshot replaces the call view.
|
|
543
|
-
*/
|
|
544
|
-
type TaskRenderSection = { lines: readonly string[] };
|
|
545
|
-
type ContextSectionRenderer = (width: number) => TaskRenderSection;
|
|
566
|
+
/** One renderable frame section: optional label, body rows, leading divider. */
|
|
567
|
+
type TaskRenderSection = { label?: string; lines: readonly string[]; separator?: boolean };
|
|
568
|
+
type AssignmentSectionRenderer = (width: number) => TaskRenderSection;
|
|
546
569
|
|
|
547
570
|
// Default output-block layout is: left border + one-cell content inset + right
|
|
548
571
|
// border. Render markdown at that inner width so the output block does not need
|
|
549
|
-
// to rewrap already-rendered
|
|
550
|
-
const
|
|
572
|
+
// to rewrap already-rendered assignment lines.
|
|
573
|
+
const ASSIGNMENT_FRAME_INSET = 3;
|
|
551
574
|
|
|
552
|
-
|
|
553
|
-
|
|
575
|
+
/**
|
|
576
|
+
* Build the assignment section (the markdown brief handed to the subagent).
|
|
577
|
+
* Rendered in both the streaming call preview and the result frame so the
|
|
578
|
+
* brief stays visible for the whole task lifecycle — not just until the first
|
|
579
|
+
* progress snapshot replaces the call view.
|
|
580
|
+
*/
|
|
581
|
+
function createAssignmentSectionRenderer(
|
|
582
|
+
args: Partial<TaskParams> | undefined,
|
|
583
|
+
theme: Theme,
|
|
584
|
+
): AssignmentSectionRenderer | undefined {
|
|
585
|
+
// `renderResult` receives the raw tool args (unlike `renderCall`, which is
|
|
586
|
+
// fed through `repairTaskParams`), so undo any per-field double-encoding
|
|
587
|
+
// here too. The repair is idempotent on already-clean text.
|
|
588
|
+
const assignment = repairDoubleEncodedJsonString(typeof args?.assignment === "string" ? args.assignment : "").trim();
|
|
589
|
+
if (!assignment) return undefined;
|
|
590
|
+
return createMarkdownSectionRenderer(assignment, theme);
|
|
554
591
|
}
|
|
555
592
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
593
|
+
/**
|
|
594
|
+
* Build the shared-context section (the `# Goal / # Constraints` background a
|
|
595
|
+
* batch call hands every subagent). Rendered like the assignment brief so the
|
|
596
|
+
* shared background stays visible for the whole task lifecycle.
|
|
597
|
+
*/
|
|
598
|
+
function createContextSectionRenderer(
|
|
599
|
+
args: Partial<TaskParams> | undefined,
|
|
600
|
+
theme: Theme,
|
|
601
|
+
): AssignmentSectionRenderer | undefined {
|
|
602
|
+
const context = repairDoubleEncodedJsonString(typeof args?.context === "string" ? args.context : "").trim();
|
|
561
603
|
if (!context) return undefined;
|
|
604
|
+
return createMarkdownSectionRenderer(context, theme);
|
|
605
|
+
}
|
|
562
606
|
|
|
563
|
-
|
|
564
|
-
|
|
607
|
+
function createMarkdownSectionRenderer(text: string, theme: Theme): AssignmentSectionRenderer {
|
|
608
|
+
const markdown = new Markdown(text, 0, 0, getMarkdownTheme(), {
|
|
609
|
+
color: line => theme.fg("muted", line),
|
|
565
610
|
});
|
|
566
|
-
return width => ({ lines: markdown.render(
|
|
611
|
+
return width => ({ lines: markdown.render(Math.max(1, width - ASSIGNMENT_FRAME_INSET)) });
|
|
567
612
|
}
|
|
568
613
|
|
|
569
614
|
/**
|
|
@@ -576,21 +621,28 @@ export function renderCall(
|
|
|
576
621
|
): Component {
|
|
577
622
|
const showIsolated = "isolated" in args && args.isolated === true;
|
|
578
623
|
const header = renderStatusLine({ icon: "pending", title: "Task", description: args.agent }, theme);
|
|
579
|
-
const
|
|
624
|
+
const assignmentSection = createAssignmentSectionRenderer(args, theme);
|
|
625
|
+
const contextSection = createContextSectionRenderer(args, theme);
|
|
580
626
|
return framedBlock(theme, width => {
|
|
581
627
|
const sections: Array<{ label?: string; lines: readonly string[]; separator?: boolean }> = [];
|
|
582
628
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
//
|
|
586
|
-
//
|
|
587
|
-
// draws the same agents as progress/result lines, so showing the Tasks
|
|
588
|
-
// section here would just repeat the count the result frame already shows.
|
|
629
|
+
// The call preview only exists to surface the dispatched agent while the
|
|
630
|
+
// args stream in. Once a result snapshot exists, `renderResult` draws the
|
|
631
|
+
// same agent (and the assignment brief) itself, so showing it here would
|
|
632
|
+
// repeat what the result frame already shows.
|
|
589
633
|
if (!options.renderContext?.hasResult) {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
634
|
+
// Mirror renderResult's layout — context, assignment, then the
|
|
635
|
+
// per-agent list — so the agent rows do not jump from above the
|
|
636
|
+
// brief to below it when the first progress snapshot replaces the
|
|
637
|
+
// call view. This also matches the schema's field order (`context`
|
|
638
|
+
// streams before `tasks`), so the streaming preview grows
|
|
639
|
+
// append-only instead of inserting agent rows above the
|
|
640
|
+
// already-rendered markdown and pushing it down on every item.
|
|
641
|
+
if (contextSection) sections.push(contextSection(width));
|
|
642
|
+
if (assignmentSection) sections.push(assignmentSection(width));
|
|
643
|
+
const callLines = renderTaskCallLines(args, theme);
|
|
644
|
+
// Guarded: an empty trailing section would still draw its divider.
|
|
645
|
+
if (callLines.length > 0) sections.push({ separator: true, lines: callLines });
|
|
594
646
|
}
|
|
595
647
|
|
|
596
648
|
return {
|
|
@@ -631,8 +683,12 @@ function renderAgentProgress(
|
|
|
631
683
|
const titlePart = description ? `${theme.bold(displayId)}: ${description}` : displayId;
|
|
632
684
|
const indent = prefix ? `${prefix} ` : "";
|
|
633
685
|
let statusLine: string;
|
|
634
|
-
if (progress.status === "running") {
|
|
635
|
-
|
|
686
|
+
if (progress.status === "running" || progress.status === "pending") {
|
|
687
|
+
// Live (or queued) agents shimmer their description so the row reads as
|
|
688
|
+
// in-flight even after the block freezes — the async spawn result keeps
|
|
689
|
+
// the agent on "pending" while the detached job runs.
|
|
690
|
+
const bullet =
|
|
691
|
+
progress.status === "running" ? theme.styledSymbol("status.done", "text") : theme.fg(iconColor, icon);
|
|
636
692
|
const name = theme.fg("accent", description ? theme.bold(displayId) : displayId);
|
|
637
693
|
statusLine = `${indent}${bullet} ${name}`;
|
|
638
694
|
if (description) {
|
|
@@ -945,6 +1001,7 @@ function renderAgentResult(
|
|
|
945
1001
|
statusLine,
|
|
946
1002
|
{
|
|
947
1003
|
tokens: result.tokens,
|
|
1004
|
+
requests: result.requests,
|
|
948
1005
|
contextTokens: result.contextTokens,
|
|
949
1006
|
contextWindow: result.contextWindow,
|
|
950
1007
|
cost: result.usage?.cost.total ?? 0,
|
|
@@ -1073,9 +1130,11 @@ function renderAgentResult(
|
|
|
1073
1130
|
}
|
|
1074
1131
|
|
|
1075
1132
|
/**
|
|
1076
|
-
* Order live progress entries so finished agents render first
|
|
1077
|
-
*
|
|
1078
|
-
*
|
|
1133
|
+
* Order live progress entries so finished agents render first — sorted by
|
|
1134
|
+
* runtime ascending, matching {@link orderResultsForDisplay} — while
|
|
1135
|
+
* unfinished (pending/running) ones stay pinned at the bottom in dispatch
|
|
1136
|
+
* order. Because a finished agent's runtime is fixed, finalization renders
|
|
1137
|
+
* the same order and rows never reshuffle.
|
|
1079
1138
|
*/
|
|
1080
1139
|
function orderProgressForDisplay(progress: readonly AgentProgress[]): AgentProgress[] {
|
|
1081
1140
|
const finished: AgentProgress[] = [];
|
|
@@ -1083,9 +1142,19 @@ function orderProgressForDisplay(progress: readonly AgentProgress[]): AgentProgr
|
|
|
1083
1142
|
for (const p of progress) {
|
|
1084
1143
|
(p.status === "pending" || p.status === "running" ? unfinished : finished).push(p);
|
|
1085
1144
|
}
|
|
1145
|
+
finished.sort((a, b) => a.durationMs - b.durationMs || a.index - b.index);
|
|
1086
1146
|
return finished.concat(unfinished);
|
|
1087
1147
|
}
|
|
1088
1148
|
|
|
1149
|
+
/**
|
|
1150
|
+
* Order finalized results by runtime ascending (tie-break: dispatch index) so
|
|
1151
|
+
* the finalized list matches the live-progress order produced by
|
|
1152
|
+
* {@link orderProgressForDisplay}.
|
|
1153
|
+
*/
|
|
1154
|
+
function orderResultsForDisplay(results: readonly SingleResult[]): SingleResult[] {
|
|
1155
|
+
return [...results].sort((a, b) => a.durationMs - b.durationMs || a.index - b.index);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1089
1158
|
/**
|
|
1090
1159
|
* Render the tool result.
|
|
1091
1160
|
*/
|
|
@@ -1097,25 +1166,28 @@ export function renderResult(
|
|
|
1097
1166
|
): Component {
|
|
1098
1167
|
const fallbackText = result.content.find(c => c.type === "text")?.text ?? "";
|
|
1099
1168
|
const details = result.details;
|
|
1100
|
-
const
|
|
1169
|
+
const agentLabel = args?.agent?.trim() || undefined;
|
|
1170
|
+
const assignmentSection = createAssignmentSectionRenderer(args, theme);
|
|
1171
|
+
const contextSection = createContextSectionRenderer(args, theme);
|
|
1101
1172
|
|
|
1102
1173
|
if (!details) {
|
|
1103
1174
|
const text = result.content.find(c => c.type === "text")?.text || "";
|
|
1104
1175
|
const errored = result.isError === true;
|
|
1105
1176
|
const header = errored
|
|
1106
|
-
? renderStatusLine({ icon: "error", title: "Task", description:
|
|
1177
|
+
? renderStatusLine({ icon: "error", title: "Task", description: agentLabel }, theme)
|
|
1107
1178
|
: renderStatusLine(
|
|
1108
1179
|
{
|
|
1109
1180
|
iconOverride: theme.styledSymbol("status.done", "accent"),
|
|
1110
1181
|
title: "Task",
|
|
1111
|
-
description:
|
|
1182
|
+
description: agentLabel,
|
|
1112
1183
|
},
|
|
1113
1184
|
theme,
|
|
1114
1185
|
);
|
|
1115
1186
|
return framedBlock(theme, width => ({
|
|
1116
1187
|
header,
|
|
1117
1188
|
sections: [
|
|
1118
|
-
...(
|
|
1189
|
+
...(contextSection ? [contextSection(width)] : []),
|
|
1190
|
+
...(assignmentSection ? [assignmentSection(width)] : []),
|
|
1119
1191
|
...(text ? [{ separator: true, lines: [theme.fg("dim", truncateToWidth(text, width))] }] : []),
|
|
1120
1192
|
],
|
|
1121
1193
|
state: errored ? "error" : "success",
|
|
@@ -1131,12 +1203,10 @@ export function renderResult(
|
|
|
1131
1203
|
const isError = aborted || failed;
|
|
1132
1204
|
const agentCount = hasResults ? details.results.length : (details.progress?.length ?? 0);
|
|
1133
1205
|
const icon: ToolUIStatus = options.isPartial ? "running" : isError ? "error" : mergeFailed ? "warning" : "success";
|
|
1134
|
-
// Surface the dispatched agent type (e.g. `Reviewer`) alongside the count
|
|
1135
|
-
// the header reads `Task
|
|
1136
|
-
// single `agent` type (top-level param), so one label covers the whole batch.
|
|
1137
|
-
const agentName = args?.agent?.trim();
|
|
1206
|
+
// Surface the dispatched agent type (e.g. `Reviewer`) alongside the count
|
|
1207
|
+
// so the header reads `Task 1 agent: Reviewer`.
|
|
1138
1208
|
const countLabel = agentCount > 0 ? `${agentCount} ${agentCount === 1 ? "agent" : "agents"}` : undefined;
|
|
1139
|
-
const metaLabel = countLabel ? (
|
|
1209
|
+
const metaLabel = countLabel ? (agentLabel ? `${countLabel}: ${agentLabel}` : countLabel) : agentLabel;
|
|
1140
1210
|
const header = renderStatusLine(
|
|
1141
1211
|
{
|
|
1142
1212
|
icon: icon === "success" ? undefined : icon,
|
|
@@ -1158,7 +1228,7 @@ export function renderResult(
|
|
|
1158
1228
|
lines.push(...renderAgentProgress(progress, "", " ", expanded, theme, spinnerFrame));
|
|
1159
1229
|
});
|
|
1160
1230
|
} else if (details.results && details.results.length > 0) {
|
|
1161
|
-
details.results.forEach(res => {
|
|
1231
|
+
orderResultsForDisplay(details.results).forEach(res => {
|
|
1162
1232
|
lines.push(...renderAgentResult(res, "", " ", expanded, theme));
|
|
1163
1233
|
});
|
|
1164
1234
|
|
|
@@ -1171,6 +1241,8 @@ export function renderResult(
|
|
|
1171
1241
|
if (successCount > 0) summaryParts.push(theme.fg("success", `${successCount} succeeded`));
|
|
1172
1242
|
if (mergeFailedCount > 0) summaryParts.push(theme.fg("warning", `${mergeFailedCount} merge failed`));
|
|
1173
1243
|
if (failCount > 0) summaryParts.push(theme.fg("error", `${failCount} failed`));
|
|
1244
|
+
const totalRequests = details.results.reduce((sum, r) => sum + (r.requests ?? 0), 0);
|
|
1245
|
+
if (totalRequests > 0) summaryParts.push(theme.fg("dim", `${formatNumber(totalRequests)} req`));
|
|
1174
1246
|
summaryParts.push(theme.fg("dim", formatDuration(details.totalDurationMs)));
|
|
1175
1247
|
// Wrap the run summary in the theme's bracket glyphs (dim chrome, colored
|
|
1176
1248
|
// counts) to match the bash tool's `[Wall: … | Exit: …]` footer.
|
|
@@ -1189,7 +1261,8 @@ export function renderResult(
|
|
|
1189
1261
|
return {
|
|
1190
1262
|
header,
|
|
1191
1263
|
sections: [
|
|
1192
|
-
...(
|
|
1264
|
+
...(contextSection ? [contextSection(width)] : []),
|
|
1265
|
+
...(assignmentSection ? [assignmentSection(width)] : []),
|
|
1193
1266
|
{ separator: true, lines: [theme.fg("dim", truncateToWidth(text, width))] },
|
|
1194
1267
|
],
|
|
1195
1268
|
state,
|
|
@@ -1219,7 +1292,8 @@ export function renderResult(
|
|
|
1219
1292
|
return {
|
|
1220
1293
|
header,
|
|
1221
1294
|
sections: [
|
|
1222
|
-
...(
|
|
1295
|
+
...(contextSection ? [contextSection(width)] : []),
|
|
1296
|
+
...(assignmentSection ? [assignmentSection(width)] : []),
|
|
1223
1297
|
...(lines.length > 0 ? [{ separator: true, lines }] : []),
|
|
1224
1298
|
],
|
|
1225
1299
|
state,
|
|
@@ -1252,8 +1326,9 @@ function renderNestedTaskResults(detailsList: TaskToolDetails[], expanded: boole
|
|
|
1252
1326
|
const lines: string[] = [];
|
|
1253
1327
|
for (const details of detailsList) {
|
|
1254
1328
|
if (!details.results || details.results.length === 0) continue;
|
|
1255
|
-
details.results
|
|
1256
|
-
|
|
1329
|
+
const ordered = orderResultsForDisplay(details.results);
|
|
1330
|
+
ordered.forEach((result, index) => {
|
|
1331
|
+
const { prefix, continuePrefix } = nestedMarkers(index === ordered.length - 1, theme);
|
|
1257
1332
|
lines.push(...renderAgentResult(result, prefix, continuePrefix, expanded, theme));
|
|
1258
1333
|
});
|
|
1259
1334
|
}
|
|
@@ -1275,8 +1350,9 @@ function renderNestedTaskTree(
|
|
|
1275
1350
|
for (const details of detailsList) {
|
|
1276
1351
|
const hasResults = Boolean(details.results && details.results.length > 0);
|
|
1277
1352
|
if (hasResults) {
|
|
1278
|
-
details.results
|
|
1279
|
-
|
|
1353
|
+
const ordered = orderResultsForDisplay(details.results);
|
|
1354
|
+
ordered.forEach((result, index) => {
|
|
1355
|
+
const { prefix, continuePrefix } = nestedMarkers(index === ordered.length - 1, theme);
|
|
1280
1356
|
lines.push(...renderAgentResult(result, prefix, continuePrefix, expanded, theme));
|
|
1281
1357
|
});
|
|
1282
1358
|
continue;
|
package/src/task/repair-args.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Repair double-encoded JSON string arguments for the task tool.
|
|
3
3
|
*
|
|
4
4
|
* Models occasionally JSON-escape a string value twice when emitting a
|
|
5
|
-
* `task` tool call, so
|
|
5
|
+
* `task` tool call, so an `assignment` that should read
|
|
6
6
|
*
|
|
7
7
|
* # Role
|
|
8
8
|
* You are a judge … "describe this" … return —
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* string.
|
|
25
25
|
*
|
|
26
26
|
* This is deliberately scoped to the task tool's natural-language fields
|
|
27
|
-
* (`
|
|
27
|
+
* (`assignment`, `description`). It is NOT applied to code-bearing
|
|
28
28
|
* tools (write/edit/bash/search), where a backslash or quote is load-bearing
|
|
29
29
|
* and a false-positive unescape would silently corrupt a file or command.
|
|
30
30
|
*/
|
|
@@ -90,15 +90,20 @@ function repairTaskItem(task: TaskItem): TaskItem {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
|
-
* Repair double-encoded prose in task-tool params (`
|
|
94
|
-
* `
|
|
95
|
-
* so callers can cheaply skip
|
|
96
|
-
* (missing/undefined fields,
|
|
97
|
-
* path as well as on
|
|
93
|
+
* Repair double-encoded prose in task-tool params (`assignment`,
|
|
94
|
+
* `description`, shared `context`, and each batch task item's prose fields).
|
|
95
|
+
* Returns the same reference when nothing changed so callers can cheaply skip
|
|
96
|
+
* work. Defensive against partially-streamed args (missing/undefined fields,
|
|
97
|
+
* partial task arrays) so it is safe on the render path as well as on
|
|
98
|
+
* execution.
|
|
98
99
|
*/
|
|
99
100
|
export function repairTaskParams(params: TaskParams): TaskParams {
|
|
100
101
|
if (params === null || typeof params !== "object") return params;
|
|
101
102
|
|
|
103
|
+
const assignment =
|
|
104
|
+
typeof params.assignment === "string" ? repairDoubleEncodedJsonString(params.assignment) : params.assignment;
|
|
105
|
+
const description =
|
|
106
|
+
typeof params.description === "string" ? repairDoubleEncodedJsonString(params.description) : params.description;
|
|
102
107
|
const context = typeof params.context === "string" ? repairDoubleEncodedJsonString(params.context) : params.context;
|
|
103
108
|
|
|
104
109
|
let tasks = params.tasks;
|
|
@@ -112,6 +117,13 @@ export function repairTaskParams(params: TaskParams): TaskParams {
|
|
|
112
117
|
if (changed) tasks = repaired;
|
|
113
118
|
}
|
|
114
119
|
|
|
115
|
-
if (
|
|
116
|
-
|
|
120
|
+
if (
|
|
121
|
+
assignment === params.assignment &&
|
|
122
|
+
description === params.description &&
|
|
123
|
+
context === params.context &&
|
|
124
|
+
tasks === params.tasks
|
|
125
|
+
) {
|
|
126
|
+
return params;
|
|
127
|
+
}
|
|
128
|
+
return { ...params, assignment, description, context, tasks };
|
|
117
129
|
}
|
package/src/task/types.ts
CHANGED
|
@@ -3,7 +3,6 @@ import type { Usage } from "@oh-my-pi/pi-ai";
|
|
|
3
3
|
import { $env } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import * as z from "zod/v4";
|
|
5
5
|
import type { AgentSessionEvent } from "../session/agent-session";
|
|
6
|
-
import { getTaskSimpleModeCapabilities, type TaskSimpleMode } from "./simple-mode";
|
|
7
6
|
import type { NestedRepoPatch } from "./worktree";
|
|
8
7
|
|
|
9
8
|
/** Source of an agent definition */
|
|
@@ -66,84 +65,88 @@ export interface SubagentLifecyclePayload {
|
|
|
66
65
|
index: number;
|
|
67
66
|
}
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
});
|
|
90
|
-
if (contextEnabled) {
|
|
91
|
-
schema = schema.extend({
|
|
92
|
-
context: z.string().optional().describe("shared background prepended to each assignment"),
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (customSchemaEnabled) {
|
|
97
|
-
schema = schema.extend({
|
|
98
|
-
schema: z.string().optional().describe("jtd schema for expected response shape"),
|
|
99
|
-
});
|
|
100
|
-
}
|
|
68
|
+
/**
|
|
69
|
+
* One unit of work. The single-spawn schema is `{ agent, ...taskItemSchema }`;
|
|
70
|
+
* the batch schema (`task.batch`) is `{ agent, context, tasks: taskItemSchema[] }`.
|
|
71
|
+
* When task isolation is enabled, `isolated` joins the item shape (per-item in
|
|
72
|
+
* batch form, top-level in the flat form via the spread).
|
|
73
|
+
*/
|
|
74
|
+
const taskItemShape = {
|
|
75
|
+
id: z.string().max(48).optional().describe("stable agent id; default generated"),
|
|
76
|
+
description: z.string().optional().describe("ui label, not seen by subagent"),
|
|
77
|
+
assignment: z.string().describe("the work; self-contained instructions"),
|
|
78
|
+
};
|
|
79
|
+
const isolatedShape = {
|
|
80
|
+
isolated: z.boolean().optional().describe("run in isolated env; returns patches"),
|
|
81
|
+
};
|
|
82
|
+
const agentShape = {
|
|
83
|
+
agent: z.string().describe("agent type to spawn"),
|
|
84
|
+
};
|
|
85
|
+
const contextShape = {
|
|
86
|
+
context: z.string().describe("shared background prepended to each assignment"),
|
|
87
|
+
};
|
|
101
88
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
isolated: z.boolean().optional().describe("run in isolated env; returns patches"),
|
|
105
|
-
});
|
|
106
|
-
}
|
|
89
|
+
export const taskItemSchema = z.object(taskItemShape);
|
|
90
|
+
const taskItemSchemaIsolated = z.object({ ...taskItemShape, ...isolatedShape });
|
|
107
91
|
|
|
108
|
-
|
|
109
|
-
|
|
92
|
+
/** Single task item. Fields are optional defensively: args stream in token by token. */
|
|
93
|
+
export interface TaskItem {
|
|
94
|
+
/** Stable agent id; default = generated AdjectiveNoun. */
|
|
95
|
+
id?: string;
|
|
96
|
+
/** UI label, not seen by the subagent. */
|
|
97
|
+
description?: string;
|
|
98
|
+
/** The work; required by the schema. */
|
|
99
|
+
assignment?: string;
|
|
100
|
+
/** Run this spawn in an isolated worktree (batch form; flat form carries it top-level). */
|
|
101
|
+
isolated?: boolean;
|
|
102
|
+
}
|
|
110
103
|
|
|
111
|
-
export const taskSchema =
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
] as const;
|
|
104
|
+
export const taskSchema = z.object({ ...agentShape, ...taskItemShape, ...isolatedShape });
|
|
105
|
+
const taskSchemaNoIsolation = z.object({ ...agentShape, ...taskItemShape });
|
|
106
|
+
const taskSchemaBatch = z.object({
|
|
107
|
+
...agentShape,
|
|
108
|
+
...contextShape,
|
|
109
|
+
tasks: z.array(taskItemSchemaIsolated).describe("tasks to spawn; one subagent per item"),
|
|
110
|
+
});
|
|
111
|
+
const taskSchemaBatchNoIsolation = z.object({
|
|
112
|
+
...agentShape,
|
|
113
|
+
...contextShape,
|
|
114
|
+
tasks: z.array(taskItemSchema).describe("tasks to spawn; one subagent per item"),
|
|
115
|
+
});
|
|
116
|
+
const ALL_TASK_SCHEMAS = [taskSchema, taskSchemaNoIsolation, taskSchemaBatch, taskSchemaBatchNoIsolation] as const;
|
|
125
117
|
|
|
126
118
|
type DynamicTaskSchema = (typeof ALL_TASK_SCHEMAS)[number];
|
|
127
119
|
export type TaskSchema = typeof taskSchema;
|
|
128
|
-
/** Active task tool parameter schema for the current
|
|
120
|
+
/** Active task tool parameter schema for the current isolation / batch flags */
|
|
129
121
|
export type TaskToolSchemaInstance = DynamicTaskSchema;
|
|
130
122
|
|
|
131
|
-
export function getTaskSchema(options: { isolationEnabled: boolean;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return options.isolationEnabled ? taskSchemaSchemaFree : taskSchemaSchemaFreeNoIsolation;
|
|
135
|
-
case "independent":
|
|
136
|
-
return options.isolationEnabled ? taskSchemaIndependent : taskSchemaIndependentNoIsolation;
|
|
137
|
-
default:
|
|
138
|
-
return options.isolationEnabled ? taskSchema : taskSchemaNoIsolation;
|
|
123
|
+
export function getTaskSchema(options: { isolationEnabled: boolean; batchEnabled: boolean }): DynamicTaskSchema {
|
|
124
|
+
if (options.batchEnabled) {
|
|
125
|
+
return options.isolationEnabled ? taskSchemaBatch : taskSchemaBatchNoIsolation;
|
|
139
126
|
}
|
|
127
|
+
return options.isolationEnabled ? taskSchema : taskSchemaNoIsolation;
|
|
140
128
|
}
|
|
141
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Runtime params union over both wire shapes. The model sees exactly one shape
|
|
132
|
+
* (`{ agent, context, tasks[] }` when `task.batch` is on, `{ agent, ...item }`
|
|
133
|
+
* otherwise); runtime stays permissive so internal callers and stale
|
|
134
|
+
* transcripts using the flat form keep working under either setting.
|
|
135
|
+
*/
|
|
142
136
|
export interface TaskParams {
|
|
143
|
-
|
|
137
|
+
/** Agent type; required. */
|
|
138
|
+
agent?: string;
|
|
139
|
+
/** Stable agent id (flat form); default = generated AdjectiveNoun. */
|
|
140
|
+
id?: string;
|
|
141
|
+
/** UI label (flat form), not seen by the subagent. */
|
|
142
|
+
description?: string;
|
|
143
|
+
/** The work (flat form). */
|
|
144
|
+
assignment?: string;
|
|
145
|
+
/** Batch form (`task.batch`): one subagent per item. */
|
|
146
|
+
tasks?: TaskItem[];
|
|
147
|
+
/** Batch form: shared background prepended to every assignment; required by the batch schema. */
|
|
144
148
|
context?: string;
|
|
145
|
-
|
|
146
|
-
tasks: TaskItem[];
|
|
149
|
+
/** Run in an isolated worktree (flat form; per-item in batch form). */
|
|
147
150
|
isolated?: boolean;
|
|
148
151
|
}
|
|
149
152
|
|
|
@@ -206,6 +209,8 @@ export interface AgentProgress {
|
|
|
206
209
|
recentTools: Array<{ tool: string; args: string; endMs: number }>;
|
|
207
210
|
recentOutput: string[];
|
|
208
211
|
toolCount: number;
|
|
212
|
+
/** Count of assistant requests (assistant message_end events) across the run. Drives the soft request budget guard. */
|
|
213
|
+
requests: number;
|
|
209
214
|
/** Cumulative input + output + cacheWrite tokens across all turns. Excludes cacheRead (re-reads cached context every turn, making cumulative sum misleading). */
|
|
210
215
|
tokens: number;
|
|
211
216
|
/**
|
|
@@ -276,6 +281,8 @@ export interface SingleResult {
|
|
|
276
281
|
durationMs: number;
|
|
277
282
|
/** Cumulative input + output + cacheWrite tokens across all turns. Excludes cacheRead (re-reads cached context every turn, making cumulative sum misleading). */
|
|
278
283
|
tokens: number;
|
|
284
|
+
/** Count of assistant requests (assistant message_end events) across the run. */
|
|
285
|
+
requests: number;
|
|
279
286
|
/** Latest per-turn context size at task completion. See `AgentProgress.contextTokens`. */
|
|
280
287
|
contextTokens?: number;
|
|
281
288
|
/** Model's context window in tokens, when known. */
|
package/src/tools/ask.ts
CHANGED
|
@@ -104,7 +104,7 @@ const RECOMMENDED_SUFFIX = " (Recommended)";
|
|
|
104
104
|
const TIMEOUT_DETECTION_TOLERANCE_MS = 1_000;
|
|
105
105
|
|
|
106
106
|
function getDoneOptionLabel(): string {
|
|
107
|
-
return `${theme.
|
|
107
|
+
return `${theme.status.success} Done selecting`;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
/** Add "(Recommended)" suffix to the option at the given index if not already present */
|
|
@@ -694,7 +694,9 @@ function normalizeRenderQuestions(raw: unknown): NonNullable<AskRenderArgs["ques
|
|
|
694
694
|
/** Render a custom free-text answer as a status line plus indented continuation rows. */
|
|
695
695
|
function renderCustomInputLines(uiTheme: Theme, customInput: string): string[] {
|
|
696
696
|
const lines = customInput.split("\n");
|
|
697
|
-
const out: string[] = [
|
|
697
|
+
const out: string[] = [
|
|
698
|
+
` ${uiTheme.styledSymbol("status.success", "success")} ${uiTheme.fg("toolOutput", lines[0] ?? "")}`,
|
|
699
|
+
];
|
|
698
700
|
for (let i = 1; i < lines.length; i++) out.push(` ${uiTheme.fg("toolOutput", lines[i])}`);
|
|
699
701
|
return out;
|
|
700
702
|
}
|