@oh-my-pi/pi-coding-agent 16.4.4 → 16.4.6
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 +62 -0
- package/dist/cli.js +3799 -3729
- package/dist/types/async/job-manager.d.ts +8 -0
- package/dist/types/cli/bench-cli.d.ts +1 -7
- package/dist/types/cli/usage-cli.d.ts +1 -0
- package/dist/types/commands/usage.d.ts +7 -0
- package/dist/types/config/settings-schema.d.ts +19 -9
- package/dist/types/config/settings.d.ts +3 -2
- package/dist/types/discovery/helpers.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +36 -0
- package/dist/types/irc/bus.d.ts +4 -0
- package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +1 -0
- package/dist/types/modes/components/ask-dialog.d.ts +27 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -8
- package/dist/types/modes/components/index.d.ts +2 -1
- package/dist/types/modes/components/model-browser.d.ts +100 -0
- package/dist/types/modes/components/model-hub.d.ts +52 -0
- package/dist/types/modes/components/pause-screen.d.ts +43 -0
- package/dist/types/modes/components/session-selector.d.ts +13 -0
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
- package/dist/types/modes/controllers/input-controller.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/queue-input.d.ts +8 -0
- package/dist/types/modes/shared.d.ts +1 -1
- package/dist/types/modes/types.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/agent-storage.d.ts +57 -0
- package/dist/types/session/session-context.d.ts +9 -0
- package/dist/types/task/executor.d.ts +26 -13
- package/dist/types/task/index.d.ts +12 -11
- package/dist/types/task/label.d.ts +4 -0
- package/dist/types/task/repair-args.d.ts +8 -8
- package/dist/types/task/types.d.ts +31 -56
- package/dist/types/tools/ask.d.ts +12 -0
- package/dist/types/tools/conflict-detect.d.ts +17 -1
- package/dist/types/tools/job.d.ts +16 -0
- package/package.json +12 -12
- package/scripts/build-binary.ts +0 -1
- package/scripts/compile-binary.ts +4 -3
- package/src/async/job-manager.ts +9 -0
- package/src/cli/bench-cli.ts +7 -26
- package/src/cli/usage-cli.ts +11 -0
- package/src/commands/usage.ts +13 -2
- package/src/commit/agentic/tools/analyze-file.ts +2 -3
- package/src/config/settings-schema.ts +18 -7
- package/src/config/settings.ts +13 -4
- package/src/discovery/helpers.ts +3 -4
- package/src/extensibility/custom-tools/loader.ts +70 -37
- package/src/extensibility/extensions/types.ts +46 -0
- package/src/irc/bus.ts +61 -20
- package/src/modes/components/__tests__/pause-screen.test.ts +143 -0
- package/src/modes/components/advisor-config.ts +32 -22
- package/src/modes/components/ask-dialog.ts +888 -0
- package/src/modes/components/custom-editor.test.ts +58 -1
- package/src/modes/components/custom-editor.ts +42 -11
- package/src/modes/components/index.ts +2 -1
- package/src/modes/components/model-browser.ts +769 -0
- package/src/modes/components/model-hub.ts +2002 -0
- package/src/modes/components/pause-screen.ts +208 -0
- package/src/modes/components/session-selector.ts +299 -42
- package/src/modes/components/tool-execution.ts +2 -0
- package/src/modes/components/usage-row.ts +5 -6
- package/src/modes/controllers/event-controller.ts +8 -2
- package/src/modes/controllers/extension-ui-controller.ts +252 -5
- package/src/modes/controllers/input-controller.ts +140 -6
- package/src/modes/controllers/selector-controller.ts +160 -97
- package/src/modes/controllers/tan-command-controller.ts +1 -1
- package/src/modes/controllers/todo-command-controller.ts +1 -2
- package/src/modes/interactive-mode.ts +8 -0
- package/src/modes/queue-input.ts +132 -0
- package/src/modes/shared.ts +1 -1
- package/src/modes/theme/theme.ts +3 -3
- package/src/modes/types.ts +4 -0
- package/src/modes/utils/ui-helpers.ts +50 -24
- package/src/prompts/agents/scout.md +0 -1
- package/src/prompts/agents/task.md +1 -1
- package/src/prompts/system/subagent-system-prompt.md +1 -5
- package/src/prompts/system/subagent-yield-reminder.md +10 -0
- package/src/prompts/system/task-label.md +23 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/prompts/tools/task-summary.md +3 -0
- package/src/prompts/tools/task.md +17 -18
- package/src/session/agent-session.ts +186 -49
- package/src/session/agent-storage.ts +330 -3
- package/src/session/history-storage.ts +1 -34
- package/src/session/session-context.test.ts +73 -0
- package/src/session/session-context.ts +43 -26
- package/src/slash-commands/builtin-registry.ts +18 -0
- package/src/task/agents.ts +2 -0
- package/src/task/executor.ts +159 -46
- package/src/task/index.ts +377 -239
- package/src/task/label.ts +38 -0
- package/src/task/render.ts +74 -22
- package/src/task/repair-args.ts +20 -31
- package/src/task/spawn-policy.test.ts +4 -4
- package/src/task/types.ts +46 -66
- package/src/tools/ask.ts +233 -40
- package/src/tools/conflict-detect.ts +102 -5
- package/src/tools/index.ts +1 -0
- package/src/tools/irc.ts +20 -11
- package/src/tools/job.ts +158 -18
- package/src/tools/write.ts +70 -6
- package/src/vibe/runtime.ts +1 -1
- package/src/web/search/providers/browser-headers.ts +30 -13
- package/dist/types/modes/components/model-selector.d.ts +0 -37
- package/dist/types/tools/bash-command-fixup.d.ts +0 -3
- package/src/modes/components/model-selector.ts +0 -1291
- package/src/tools/bash-command-fixup.ts +0 -4
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny-model UI labels for spawned subagents.
|
|
3
|
+
*/
|
|
4
|
+
import { logger, prompt } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import type { ModelRegistry } from "../config/model-registry";
|
|
6
|
+
import type { Settings } from "../config/settings";
|
|
7
|
+
import taskLabelSystemPrompt from "../prompts/system/task-label.md" with { type: "text" };
|
|
8
|
+
import { generateSessionTitle } from "../utils/title-generator";
|
|
9
|
+
|
|
10
|
+
const TASK_LABEL_SYSTEM_PROMPT = prompt.render(taskLabelSystemPrompt);
|
|
11
|
+
|
|
12
|
+
/** Compresses a delegated assignment into a one-sentence UI label via the tiny title model — fired by the executor spawn path because the task wire schema no longer carries a `description`; null on empty input or failure. */
|
|
13
|
+
export async function generateTaskLabel(
|
|
14
|
+
assignment: string,
|
|
15
|
+
registry: ModelRegistry,
|
|
16
|
+
settings: Settings,
|
|
17
|
+
sessionId?: string,
|
|
18
|
+
): Promise<string | null> {
|
|
19
|
+
const text = assignment.trim();
|
|
20
|
+
if (!text) return null;
|
|
21
|
+
try {
|
|
22
|
+
return await generateSessionTitle(
|
|
23
|
+
text,
|
|
24
|
+
registry,
|
|
25
|
+
settings,
|
|
26
|
+
sessionId,
|
|
27
|
+
undefined,
|
|
28
|
+
undefined,
|
|
29
|
+
TASK_LABEL_SYSTEM_PROMPT,
|
|
30
|
+
);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
logger.debug("task-label: generation failed", {
|
|
33
|
+
sessionId,
|
|
34
|
+
error: err instanceof Error ? err.message : String(err),
|
|
35
|
+
});
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/task/render.ts
CHANGED
|
@@ -677,6 +677,35 @@ function formatOutputInline(data: unknown, theme: Theme, maxWidth = 80): string
|
|
|
677
677
|
return `Output: ${pairs.join(", ")}`;
|
|
678
678
|
}
|
|
679
679
|
|
|
680
|
+
/**
|
|
681
|
+
* First line of a streamed `task` brief, trimmed — a row's secondary text.
|
|
682
|
+
* The args stream in token by token, so non-string values fall through to "".
|
|
683
|
+
*/
|
|
684
|
+
function taskFirstLine(task: unknown): string {
|
|
685
|
+
if (typeof task !== "string") return "";
|
|
686
|
+
const trimmed = task.trim();
|
|
687
|
+
const newline = trimmed.indexOf("\n");
|
|
688
|
+
return newline === -1 ? trimmed : trimmed.slice(0, newline);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Header label for a task call while nothing has spawned yet: the flat form's
|
|
693
|
+
* `agent` type. Batch calls return undefined — each item row carries its own
|
|
694
|
+
* `⟨agent⟩` badge, so a joined list in the header would just repeat them.
|
|
695
|
+
*/
|
|
696
|
+
function formatAgentHeaderLabel(args: Partial<TaskParams> | undefined): string | undefined {
|
|
697
|
+
if (!args) return undefined;
|
|
698
|
+
const flat = typeof args.agent === "string" ? args.agent.trim() : "";
|
|
699
|
+
return flat || undefined;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/** Dim `⟨agent⟩` badge for a non-default agent type; empty for the generic worker. */
|
|
703
|
+
function agentTypeBadge(agent: string | undefined, theme: Theme): string {
|
|
704
|
+
const trimmed = agent?.trim();
|
|
705
|
+
if (!trimmed || trimmed === "task") return "";
|
|
706
|
+
return ` ${theme.fg("dim", `${theme.format.bracketLeft}${trimmed}${theme.format.bracketRight}`)}`;
|
|
707
|
+
}
|
|
708
|
+
|
|
680
709
|
/**
|
|
681
710
|
* Render the call preview lines for the single spawned agent. The
|
|
682
711
|
* args stream in token by token, so every field access is defensive.
|
|
@@ -686,14 +715,15 @@ function renderTaskCallLines(args: Partial<TaskParams> | undefined, theme: Theme
|
|
|
686
715
|
const bullet = theme.fg("dim", "•");
|
|
687
716
|
const lines: string[] = [];
|
|
688
717
|
|
|
689
|
-
const
|
|
690
|
-
const idLabel =
|
|
691
|
-
const
|
|
692
|
-
if (idLabel ||
|
|
718
|
+
const rawName = typeof args.name === "string" ? args.name.trim() : "";
|
|
719
|
+
const idLabel = rawName ? formatTaskId(rawName) : "";
|
|
720
|
+
const brief = taskFirstLine(args.task);
|
|
721
|
+
if (idLabel || brief) {
|
|
693
722
|
let line = `${bullet} ${theme.fg("accent", theme.bold(idLabel || "agent"))}`;
|
|
694
|
-
if (
|
|
695
|
-
line += `: ${theme.fg("muted", previewLine(
|
|
723
|
+
if (brief) {
|
|
724
|
+
line += `: ${theme.fg("muted", previewLine(brief, 64))}`;
|
|
696
725
|
}
|
|
726
|
+
line += agentTypeBadge(args.agent, theme);
|
|
697
727
|
lines.push(line);
|
|
698
728
|
}
|
|
699
729
|
lines.push(...renderTaskItemLines(args.tasks, theme));
|
|
@@ -707,7 +737,7 @@ function renderTaskCallLines(args: Partial<TaskParams> | undefined, theme: Theme
|
|
|
707
737
|
const COLLAPSED_AGENT_LIMIT = 4;
|
|
708
738
|
|
|
709
739
|
/**
|
|
710
|
-
* Render the per-item list (`
|
|
740
|
+
* Render the per-item list (`name` + `task` brief) for a batch call's
|
|
711
741
|
* streaming preview. The args stream in token by token, so the array grows
|
|
712
742
|
* over time and trailing entries may be partially parsed — every field access
|
|
713
743
|
* is defensive.
|
|
@@ -719,15 +749,16 @@ function renderTaskItemLines(tasks: TaskItem[] | undefined, theme: Theme): strin
|
|
|
719
749
|
const cap = Math.min(tasks.length, COLLAPSED_AGENT_LIMIT);
|
|
720
750
|
const lines: string[] = [];
|
|
721
751
|
for (let i = 0; i < cap; i++) {
|
|
722
|
-
const
|
|
723
|
-
const
|
|
724
|
-
const idLabel =
|
|
752
|
+
const item = tasks[i] as Partial<TaskItem> | undefined;
|
|
753
|
+
const rawName = typeof item?.name === "string" ? item.name.trim() : "";
|
|
754
|
+
const idLabel = rawName ? formatTaskId(rawName) : `#${i + 1}`;
|
|
725
755
|
let line = `${bullet} ${theme.fg("accent", theme.bold(idLabel))}`;
|
|
726
|
-
const
|
|
727
|
-
if (
|
|
728
|
-
line += `: ${theme.fg("muted", previewLine(
|
|
756
|
+
const brief = taskFirstLine(item?.task);
|
|
757
|
+
if (brief) {
|
|
758
|
+
line += `: ${theme.fg("muted", previewLine(brief, 64))}`;
|
|
729
759
|
}
|
|
730
|
-
|
|
760
|
+
line += agentTypeBadge(item?.agent, theme);
|
|
761
|
+
if (item?.isolated === true) {
|
|
731
762
|
line += theme.fg("dim", " [isolated]");
|
|
732
763
|
}
|
|
733
764
|
lines.push(line);
|
|
@@ -760,7 +791,7 @@ function createAssignmentSectionRenderer(
|
|
|
760
791
|
// `renderResult` receives the raw tool args (unlike `renderCall`, which is
|
|
761
792
|
// fed through `repairTaskParams`), so undo any per-field double-encoding
|
|
762
793
|
// here too. The repair is idempotent on already-clean text.
|
|
763
|
-
const assignment = repairDoubleEncodedJsonString(typeof args?.
|
|
794
|
+
const assignment = repairDoubleEncodedJsonString(typeof args?.task === "string" ? args.task : "").trim();
|
|
764
795
|
if (!assignment) return undefined;
|
|
765
796
|
return createMarkdownSectionRenderer(assignment, theme);
|
|
766
797
|
}
|
|
@@ -795,7 +826,11 @@ export function renderCall(args: TaskParams, options: TaskRenderOptions, theme:
|
|
|
795
826
|
// pending/hourglass icon would misread the call as something the turn
|
|
796
827
|
// waits on.
|
|
797
828
|
const header = renderStatusLine(
|
|
798
|
-
{
|
|
829
|
+
{
|
|
830
|
+
iconOverride: theme.styledSymbol("tool.task", "accent"),
|
|
831
|
+
title: "Task",
|
|
832
|
+
description: formatAgentHeaderLabel(args),
|
|
833
|
+
},
|
|
799
834
|
theme,
|
|
800
835
|
);
|
|
801
836
|
const assignmentSection = createAssignmentSectionRenderer(args, theme);
|
|
@@ -883,6 +918,7 @@ function renderAgentProgress(
|
|
|
883
918
|
} else {
|
|
884
919
|
statusLine = `${indent}${theme.fg(iconColor, icon)} ${theme.fg("accent", titlePart)}`;
|
|
885
920
|
}
|
|
921
|
+
statusLine += agentTypeBadge(progress.agent, theme);
|
|
886
922
|
|
|
887
923
|
// Show retry-blocked badge so the parent immediately sees that a child
|
|
888
924
|
// is sleeping on a provider 429, not silently progressing. Wins over the
|
|
@@ -1215,7 +1251,7 @@ function renderAgentResult(
|
|
|
1215
1251
|
let statusLine = `${prefix ? `${prefix} ` : ""}${theme.fg(iconColor, icon)} ${theme.fg(
|
|
1216
1252
|
success && !needsWarning ? "text" : "accent",
|
|
1217
1253
|
titlePart,
|
|
1218
|
-
)} ${formatBadge(statusText, iconColor, theme)}`;
|
|
1254
|
+
)}${agentTypeBadge(result.agent, theme)} ${formatBadge(statusText, iconColor, theme)}`;
|
|
1219
1255
|
const showBadge = settings.get("task.showResolvedModelBadge");
|
|
1220
1256
|
statusLine = appendAgentStats(
|
|
1221
1257
|
statusLine,
|
|
@@ -1460,7 +1496,7 @@ export function renderResult(
|
|
|
1460
1496
|
): Component {
|
|
1461
1497
|
const fallbackText = result.content.find(c => c.type === "text")?.text ?? "";
|
|
1462
1498
|
const details = result.details;
|
|
1463
|
-
const agentLabel = args
|
|
1499
|
+
const agentLabel = formatAgentHeaderLabel(args);
|
|
1464
1500
|
const assignmentSection = createAssignmentSectionRenderer(args, theme);
|
|
1465
1501
|
const contextSection = createContextSectionRenderer(args, theme);
|
|
1466
1502
|
|
|
@@ -1515,10 +1551,11 @@ export function renderResult(
|
|
|
1515
1551
|
const isError = aborted || failed;
|
|
1516
1552
|
const agentCount = hasResults ? details.results.length : (details.progress?.length ?? 0);
|
|
1517
1553
|
const icon: ToolUIStatus = options.isPartial ? "running" : isError ? "error" : mergeFailed ? "warning" : "success";
|
|
1518
|
-
//
|
|
1519
|
-
// so
|
|
1554
|
+
// Header meta is the spawn count only; each row carries its own ⟨agent⟩
|
|
1555
|
+
// badge, so a joined type list here would repeat them. Before anything
|
|
1556
|
+
// spawns, fall back to the flat form's agent type from the call args.
|
|
1520
1557
|
const countLabel = agentCount > 0 ? `${agentCount} ${agentCount === 1 ? "agent" : "agents"}` : undefined;
|
|
1521
|
-
const metaLabel = countLabel
|
|
1558
|
+
const metaLabel = countLabel ?? agentLabel;
|
|
1522
1559
|
const header = renderStatusLine(
|
|
1523
1560
|
{
|
|
1524
1561
|
icon: icon === "success" || icon === "running" ? undefined : icon,
|
|
@@ -1542,8 +1579,10 @@ export function renderResult(
|
|
|
1542
1579
|
const frozen = options.renderContext?.frozen === true;
|
|
1543
1580
|
const lines: string[] = [];
|
|
1544
1581
|
|
|
1582
|
+
// Result rows win once any exist; progress rows for spawns without a
|
|
1583
|
+
// result (a mixed call's async subset) render as a supplement below.
|
|
1545
1584
|
const shouldRenderProgress =
|
|
1546
|
-
Boolean(details.progress && details.progress.length > 0) &&
|
|
1585
|
+
Boolean(details.progress && details.progress.length > 0) && details.results.length === 0;
|
|
1547
1586
|
if (shouldRenderProgress && details.progress) {
|
|
1548
1587
|
const ordered = orderProgressForDisplay(details.progress);
|
|
1549
1588
|
// Collapsed view keeps the live edge: finished rows sort to the top of
|
|
@@ -1570,6 +1609,19 @@ export function renderResult(
|
|
|
1570
1609
|
);
|
|
1571
1610
|
}
|
|
1572
1611
|
|
|
1612
|
+
// Mixed blocking+async call: async spawns never land in `results`
|
|
1613
|
+
// (their payloads deliver through jobs) — keep their rows visible
|
|
1614
|
+
// beside the finalized inline results, live while running and
|
|
1615
|
+
// settled once their jobs finish.
|
|
1616
|
+
const supplementalProgress = details.progress
|
|
1617
|
+
? orderProgressForDisplay(
|
|
1618
|
+
details.progress.filter(progress => !details.results.some(res => res.id === progress.id)),
|
|
1619
|
+
)
|
|
1620
|
+
: [];
|
|
1621
|
+
for (const progress of supplementalProgress) {
|
|
1622
|
+
lines.push(...renderAgentProgress(progress, "", " ", expanded, theme, spinnerFrame, frozen));
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1573
1625
|
const summaryParts: string[] = [];
|
|
1574
1626
|
if (abortedCount > 0) summaryParts.push(theme.fg("error", `${abortedCount} aborted`));
|
|
1575
1627
|
if (successCount > 0) summaryParts.push(theme.fg("success", `${successCount} succeeded`));
|
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 a `task` field that should read
|
|
6
6
|
*
|
|
7
7
|
* # Role
|
|
8
8
|
* You are a judge … "describe this" … return —
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
* string.
|
|
25
25
|
*
|
|
26
26
|
* This is deliberately scoped to the task tool's natural-language fields
|
|
27
|
-
* (`
|
|
27
|
+
* (`task`, shared `context`); identifier fields (`name`, `agent`)
|
|
28
|
+
* are never repaired. It is NOT applied to code-bearing
|
|
28
29
|
* tools (write/edit/bash/search), where a backslash or quote is load-bearing
|
|
29
30
|
* and a false-positive unescape would silently corrupt a file or command.
|
|
30
31
|
*/
|
|
@@ -78,52 +79,40 @@ export function repairDoubleEncodedJsonString(value: string): string {
|
|
|
78
79
|
return typeof decoded === "string" && decoded !== value ? decoded : value;
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
/** Repair a single (possibly partial) task item's prose
|
|
82
|
-
function repairTaskItem(
|
|
83
|
-
if (
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
typeof task.description === "string" ? repairDoubleEncodedJsonString(task.description) : task.description;
|
|
88
|
-
if (assignment === task.assignment && description === task.description) return task;
|
|
89
|
-
return { ...task, assignment, description };
|
|
82
|
+
/** Repair a single (possibly partial) task item's prose field (`task`). */
|
|
83
|
+
function repairTaskItem(item: TaskItem): TaskItem {
|
|
84
|
+
if (item === null || typeof item !== "object") return item;
|
|
85
|
+
const task = typeof item.task === "string" ? repairDoubleEncodedJsonString(item.task) : item.task;
|
|
86
|
+
if (task === item.task) return item;
|
|
87
|
+
return { ...item, task };
|
|
90
88
|
}
|
|
91
89
|
|
|
92
90
|
/**
|
|
93
|
-
* Repair double-encoded prose in task-tool params (`
|
|
94
|
-
* `
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* execution.
|
|
91
|
+
* Repair double-encoded prose in task-tool params (flat `task`, shared
|
|
92
|
+
* `context`, and each batch task item's `task`). Returns the same reference
|
|
93
|
+
* when nothing changed so callers can cheaply skip work. Defensive against
|
|
94
|
+
* partially-streamed args (missing/undefined fields, partial task arrays) so
|
|
95
|
+
* it is safe on the render path as well as on execution.
|
|
99
96
|
*/
|
|
100
97
|
export function repairTaskParams(params: TaskParams): TaskParams {
|
|
101
98
|
if (params === null || typeof params !== "object") return params;
|
|
102
99
|
|
|
103
|
-
const
|
|
104
|
-
typeof params.assignment === "string" ? repairDoubleEncodedJsonString(params.assignment) : params.assignment;
|
|
105
|
-
const description =
|
|
106
|
-
typeof params.description === "string" ? repairDoubleEncodedJsonString(params.description) : params.description;
|
|
100
|
+
const task = typeof params.task === "string" ? repairDoubleEncodedJsonString(params.task) : params.task;
|
|
107
101
|
const context = typeof params.context === "string" ? repairDoubleEncodedJsonString(params.context) : params.context;
|
|
108
102
|
|
|
109
103
|
let tasks = params.tasks;
|
|
110
104
|
if (Array.isArray(params.tasks)) {
|
|
111
105
|
let changed = false;
|
|
112
|
-
const repaired = params.tasks.map(
|
|
113
|
-
const next = repairTaskItem(
|
|
114
|
-
if (next !==
|
|
106
|
+
const repaired = params.tasks.map(item => {
|
|
107
|
+
const next = repairTaskItem(item);
|
|
108
|
+
if (next !== item) changed = true;
|
|
115
109
|
return next;
|
|
116
110
|
});
|
|
117
111
|
if (changed) tasks = repaired;
|
|
118
112
|
}
|
|
119
113
|
|
|
120
|
-
if (
|
|
121
|
-
assignment === params.assignment &&
|
|
122
|
-
description === params.description &&
|
|
123
|
-
context === params.context &&
|
|
124
|
-
tasks === params.tasks
|
|
125
|
-
) {
|
|
114
|
+
if (task === params.task && context === params.context && tasks === params.tasks) {
|
|
126
115
|
return params;
|
|
127
116
|
}
|
|
128
|
-
return { ...params,
|
|
117
|
+
return { ...params, task, context, tasks };
|
|
129
118
|
}
|
|
@@ -42,9 +42,9 @@ describe("task spawn policy surfaces", () => {
|
|
|
42
42
|
|
|
43
43
|
it("uses the first allowed spawn as the schema default", () => {
|
|
44
44
|
const schema = getTaskSchema({ isolationEnabled: false, batchEnabled: false, defaultAgent: "fact-finder" });
|
|
45
|
-
const parsed = schema({
|
|
45
|
+
const parsed = schema({ task: "check" });
|
|
46
46
|
|
|
47
|
-
expect(parsed).toEqual({ agent: "fact-finder",
|
|
47
|
+
expect(parsed).toEqual({ agent: "fact-finder", task: "check" });
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
it("renders the restricted spawn default in the task description", async () => {
|
|
@@ -56,8 +56,8 @@ describe("task spawn policy surfaces", () => {
|
|
|
56
56
|
const tool = await TaskTool.create(makeSession("fact-finder,oracle"));
|
|
57
57
|
const description = tool.description;
|
|
58
58
|
|
|
59
|
-
expect(description).toContain("
|
|
59
|
+
expect(description).toContain("the general-purpose worker (`fact-finder`)");
|
|
60
60
|
expect(description).toContain("Current spawn policy allows: `fact-finder`, `oracle`.");
|
|
61
|
-
expect(description).not.toContain("
|
|
61
|
+
expect(description).not.toContain("(`task`)");
|
|
62
62
|
});
|
|
63
63
|
});
|
package/src/task/types.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
2
1
|
import type { Usage } from "@oh-my-pi/pi-ai";
|
|
3
2
|
import { $env } from "@oh-my-pi/pi-utils";
|
|
4
3
|
import { type BaseType, type } from "arktype";
|
|
5
4
|
import type { AgentSessionEvent } from "../session/agent-session";
|
|
5
|
+
import type { ConfiguredThinkingLevel } from "../thinking";
|
|
6
6
|
import type { NestedRepoPatch } from "./worktree";
|
|
7
7
|
|
|
8
8
|
/** Source of an agent definition */
|
|
@@ -75,66 +75,53 @@ export interface SubagentLifecyclePayload {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
/** Display cap for a normalized one-line label (roster line, registry `displayName`, prompt field). */
|
|
78
|
-
export const
|
|
79
|
-
/** Schema bound on the raw `role` input, before it is label-normalized at every use site. */
|
|
80
|
-
export const ROLE_INPUT_MAX = 256;
|
|
81
|
-
const ROLE_INPUT_SCHEMA = `string <= ${ROLE_INPUT_MAX}` as const;
|
|
78
|
+
export const LABEL_MAX = 80;
|
|
82
79
|
|
|
83
80
|
export const taskItemSchema = type({
|
|
84
|
-
"
|
|
85
|
-
|
|
86
|
-
"
|
|
87
|
-
assignment: "string",
|
|
81
|
+
"name?": "string",
|
|
82
|
+
agent: "string = 'task'",
|
|
83
|
+
task: "string",
|
|
88
84
|
"+": "delete",
|
|
89
85
|
});
|
|
90
86
|
const taskItemSchemaIsolated = type({
|
|
91
|
-
"
|
|
92
|
-
|
|
93
|
-
"
|
|
94
|
-
assignment: "string",
|
|
87
|
+
"name?": "string",
|
|
88
|
+
agent: "string = 'task'",
|
|
89
|
+
task: "string",
|
|
95
90
|
"isolated?": "boolean",
|
|
96
91
|
"+": "delete",
|
|
97
92
|
});
|
|
98
93
|
|
|
99
94
|
/** Single task item. Fields are optional defensively: args stream in token by token. */
|
|
100
95
|
export interface TaskItem {
|
|
101
|
-
/** Stable agent
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
|
|
105
|
-
/** Specialist role/expertise this subagent embodies; shapes its system-prompt identity and display name. */
|
|
106
|
-
role?: string;
|
|
96
|
+
/** Stable agent name; becomes the registry/IRC id. Default = generated AdjectiveNoun. */
|
|
97
|
+
name?: string;
|
|
98
|
+
/** Agent type to run this item (e.g. "scout"). Defaults to the spawn policy's default agent. */
|
|
99
|
+
agent?: string;
|
|
107
100
|
/** The work; required by the schema. */
|
|
108
|
-
|
|
101
|
+
task?: string;
|
|
109
102
|
/** Run this spawn in an isolated worktree (batch form; flat form carries it top-level). */
|
|
110
103
|
isolated?: boolean;
|
|
111
104
|
}
|
|
112
105
|
|
|
113
106
|
export const taskSchema = type({
|
|
107
|
+
"name?": "string",
|
|
114
108
|
agent: "string = 'task'",
|
|
115
|
-
|
|
116
|
-
"description?": "string",
|
|
117
|
-
"role?": ROLE_INPUT_SCHEMA,
|
|
118
|
-
assignment: "string",
|
|
109
|
+
task: "string",
|
|
119
110
|
"isolated?": "boolean",
|
|
120
111
|
"+": "delete",
|
|
121
112
|
});
|
|
122
113
|
const taskSchemaNoIsolation = type({
|
|
114
|
+
"name?": "string",
|
|
123
115
|
agent: "string = 'task'",
|
|
124
|
-
|
|
125
|
-
"description?": "string",
|
|
126
|
-
"role?": ROLE_INPUT_SCHEMA,
|
|
127
|
-
assignment: "string",
|
|
116
|
+
task: "string",
|
|
128
117
|
"+": "delete",
|
|
129
118
|
});
|
|
130
119
|
const taskSchemaBatch = type({
|
|
131
|
-
agent: "string = 'task'",
|
|
132
120
|
context: "string",
|
|
133
121
|
tasks: taskItemSchemaIsolated.array(),
|
|
134
122
|
"+": "delete",
|
|
135
123
|
});
|
|
136
124
|
const taskSchemaBatchNoIsolation = type({
|
|
137
|
-
agent: "string = 'task'",
|
|
138
125
|
context: "string",
|
|
139
126
|
tasks: taskItemSchema.array(),
|
|
140
127
|
"+": "delete",
|
|
@@ -165,37 +152,44 @@ function createTaskSchema(options: {
|
|
|
165
152
|
const agent = taskAgentSchemaRule(options.defaultAgent);
|
|
166
153
|
if (options.batchEnabled) {
|
|
167
154
|
if (options.isolationEnabled) {
|
|
168
|
-
|
|
155
|
+
const item = type.raw({
|
|
156
|
+
"name?": "string",
|
|
169
157
|
agent,
|
|
158
|
+
task: "string",
|
|
159
|
+
"isolated?": "boolean",
|
|
160
|
+
"+": "delete",
|
|
161
|
+
});
|
|
162
|
+
return type.raw({
|
|
170
163
|
context: "string",
|
|
171
|
-
tasks:
|
|
164
|
+
tasks: item.array(),
|
|
172
165
|
"+": "delete",
|
|
173
166
|
});
|
|
174
167
|
}
|
|
175
|
-
|
|
168
|
+
const item = type.raw({
|
|
169
|
+
"name?": "string",
|
|
176
170
|
agent,
|
|
171
|
+
task: "string",
|
|
172
|
+
"+": "delete",
|
|
173
|
+
});
|
|
174
|
+
return type.raw({
|
|
177
175
|
context: "string",
|
|
178
|
-
tasks:
|
|
176
|
+
tasks: item.array(),
|
|
179
177
|
"+": "delete",
|
|
180
178
|
});
|
|
181
179
|
}
|
|
182
180
|
if (options.isolationEnabled) {
|
|
183
181
|
return type.raw({
|
|
182
|
+
"name?": "string",
|
|
184
183
|
agent,
|
|
185
|
-
|
|
186
|
-
"description?": "string",
|
|
187
|
-
"role?": ROLE_INPUT_SCHEMA,
|
|
188
|
-
assignment: "string",
|
|
184
|
+
task: "string",
|
|
189
185
|
"isolated?": "boolean",
|
|
190
186
|
"+": "delete",
|
|
191
187
|
});
|
|
192
188
|
}
|
|
193
189
|
return type.raw({
|
|
190
|
+
"name?": "string",
|
|
194
191
|
agent,
|
|
195
|
-
|
|
196
|
-
"description?": "string",
|
|
197
|
-
"role?": ROLE_INPUT_SCHEMA,
|
|
198
|
-
assignment: "string",
|
|
192
|
+
task: "string",
|
|
199
193
|
"+": "delete",
|
|
200
194
|
});
|
|
201
195
|
}
|
|
@@ -226,21 +220,17 @@ export function getTaskSchema(options: {
|
|
|
226
220
|
|
|
227
221
|
/**
|
|
228
222
|
* Runtime params union over both wire shapes. The model sees exactly one shape
|
|
229
|
-
* (`{
|
|
223
|
+
* (`{ context, tasks[] }` when `task.batch` is on, `{ name?, agent?, task }`
|
|
230
224
|
* otherwise); runtime stays permissive so internal callers and stale
|
|
231
225
|
* transcripts using the flat form keep working under either setting.
|
|
232
226
|
*/
|
|
233
227
|
export interface TaskParams {
|
|
234
|
-
/**
|
|
228
|
+
/** Stable agent name (flat form). */
|
|
229
|
+
name?: string;
|
|
230
|
+
/** Agent type to spawn (flat form); omitted values resolve from the session spawn policy. */
|
|
235
231
|
agent?: string;
|
|
236
|
-
/** Stable agent id (flat form); default = generated AdjectiveNoun. */
|
|
237
|
-
id?: string;
|
|
238
|
-
/** UI label (flat form), not seen by the subagent. */
|
|
239
|
-
description?: string;
|
|
240
|
-
/** Specialist role/expertise this subagent embodies; shapes its system-prompt identity and display name. */
|
|
241
|
-
role?: string;
|
|
242
232
|
/** The work (flat form). */
|
|
243
|
-
|
|
233
|
+
task?: string;
|
|
244
234
|
/** Batch form (`task.batch`): one subagent per item. */
|
|
245
235
|
tasks?: TaskItem[];
|
|
246
236
|
/** Batch form: shared background prepended to every assignment; required by the batch schema. */
|
|
@@ -254,11 +244,11 @@ export interface TaskParams {
|
|
|
254
244
|
* `displayName`, or a system-prompt field. Collapses every run of whitespace
|
|
255
245
|
* AND control/format characters — including U+0085 NEL, ESC/ANSI, and the
|
|
256
246
|
* zero-width separators that `\s` misses — to a single space, then caps length.
|
|
257
|
-
* So untrusted text (a
|
|
258
|
-
* line, inject prompt structure, nor smuggle terminal escapes. Caps at
|
|
259
|
-
* characters (clamped to >= 1; default `
|
|
247
|
+
* So untrusted text (a generated task label, a peer activity gist) can neither
|
|
248
|
+
* break the line, inject prompt structure, nor smuggle terminal escapes. Caps at
|
|
249
|
+
* `max` characters (clamped to >= 1; default `LABEL_MAX`), appending an ellipsis when truncated.
|
|
260
250
|
*/
|
|
261
|
-
export function oneLineLabel(text: string, max =
|
|
251
|
+
export function oneLineLabel(text: string, max = LABEL_MAX): string {
|
|
262
252
|
const oneLine = text.replace(/[\p{Cc}\p{Cf}\s]+/gu, " ").trim();
|
|
263
253
|
const cap = Math.max(1, max);
|
|
264
254
|
// Count/cut by code point, not UTF-16 code unit, so truncation can never
|
|
@@ -267,16 +257,6 @@ export function oneLineLabel(text: string, max = ROLE_LABEL_MAX): string {
|
|
|
267
257
|
return chars.length > cap ? `${chars.slice(0, cap - 1).join("")}…` : oneLine;
|
|
268
258
|
}
|
|
269
259
|
|
|
270
|
-
/**
|
|
271
|
-
* Display name for a spawned subagent: its tailored `role` (label-normalized)
|
|
272
|
-
* when one is given, else the agent type's name. Empty/whitespace roles fall
|
|
273
|
-
* back to the agent name.
|
|
274
|
-
*/
|
|
275
|
-
export function resolveSubagentDisplayName(role: string | undefined, agentName: string): string {
|
|
276
|
-
const trimmed = role?.trim();
|
|
277
|
-
return trimmed ? oneLineLabel(trimmed) : agentName;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
260
|
/**
|
|
281
261
|
* Whether an agent at `taskDepth` may still spawn children — i.e. it currently
|
|
282
262
|
* holds the `task` tool. Mirrors the task-tool availability gate;
|
|
@@ -318,7 +298,7 @@ export interface AgentDefinition {
|
|
|
318
298
|
tools?: string[];
|
|
319
299
|
spawns?: string[] | "*";
|
|
320
300
|
model?: string[];
|
|
321
|
-
thinkingLevel?:
|
|
301
|
+
thinkingLevel?: ConfiguredThinkingLevel;
|
|
322
302
|
output?: unknown;
|
|
323
303
|
blocking?: boolean;
|
|
324
304
|
autoloadSkills?: string[];
|