@aeondave/pi-persona 1.17.3 → 1.17.5
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/AGENTS.md +57 -36
- package/CHANGELOG.md +106 -1
- package/README.md +32 -16
- package/docs/ARCHITECTURE.md +199 -48
- package/docs/COLLABORATION.md +110 -0
- package/docs/EXPERIENCE.md +33 -8
- package/docs/README.md +12 -0
- package/docs/REFERENCE.md +65 -14
- package/docs/STRATEGIES.md +6 -0
- package/package.json +12 -6
- package/src/agents/agent.ts +6 -0
- package/src/bridge.ts +33 -23
- package/src/bus/broker/client.ts +151 -30
- package/src/bus/broker/host.ts +130 -11
- package/src/bus/broker/messages.ts +35 -5
- package/src/bus/contact-transport.ts +8 -0
- package/src/bus/contact.ts +118 -40
- package/src/bus/peers.ts +23 -6
- package/src/bus/result-reader.ts +88 -0
- package/src/core/brief.ts +14 -4
- package/src/core/capabilities.ts +1 -1
- package/src/core/companion.ts +64 -0
- package/src/core/leg-thinking.ts +57 -0
- package/src/core/ownership.ts +1 -2
- package/src/core/pi-compat.ts +5 -12
- package/src/core/session-runs.ts +1 -1
- package/src/core/types.ts +1 -1
- package/src/engine/adapter.ts +100 -20
- package/src/engine/async.ts +53 -11
- package/src/engine/child.ts +11 -2
- package/src/engine/inproc.ts +98 -47
- package/src/engine/result-grant.ts +62 -0
- package/src/engine/session-tools.ts +6 -0
- package/src/engine/stream.ts +6 -0
- package/src/engine/worktree.ts +17 -7
- package/src/exocom/collaboration-delivery.ts +29 -0
- package/src/exocom/collaboration.ts +148 -0
- package/src/exocom/envelope.ts +43 -4
- package/src/exocom/gate.ts +7 -2
- package/src/exocom/install.ts +45 -17
- package/src/exocom/ledger.ts +58 -8
- package/src/exocom/plane.ts +32 -1
- package/src/exocom/profile.ts +40 -0
- package/src/exocom/registry.ts +5 -0
- package/src/exocom/status.ts +11 -1
- package/src/exocom/write-guard.ts +7 -0
- package/src/extension/broker-host.ts +61 -9
- package/src/extension/companion.ts +38 -0
- package/src/extension/engine.ts +40 -29
- package/src/extension/hooks.ts +954 -931
- package/src/extension/identity.ts +41 -75
- package/src/extension/memory-owner.ts +54 -0
- package/src/extension/mind-worker.ts +129 -0
- package/src/extension/results.ts +138 -0
- package/src/extension/shared.ts +17 -0
- package/src/extension.ts +2922 -2849
- package/src/loader.ts +11 -3
- package/src/orchestration/agent-attempt.ts +15 -0
- package/src/orchestration/flow-journal.ts +7 -1
- package/src/orchestration/flow-run.ts +125 -19
- package/src/orchestration/flow.ts +11 -0
- package/src/orchestration/roster.ts +71 -23
- package/src/orchestration/sdk.ts +61 -19
- package/src/orchestration/types.ts +10 -0
- package/src/persona/model-participants.ts +20 -7
- package/src/persona/orchestrate.ts +6 -7
- package/src/persona/persona.ts +1 -1
- package/src/results/archive.ts +281 -0
- package/src/results/record.ts +166 -0
- package/src/results/retrieval.ts +142 -0
- package/src/results/types.ts +128 -0
- package/src/telemetry/contract.ts +1 -1
- package/src/tools/council.ts +40 -23
- package/src/tools/delegate-tool.ts +50 -14
- package/src/tools/delegate.ts +13 -6
- package/src/tools/exocom-collaboration-input.ts +168 -0
- package/src/tools/exocom-collaboration.ts +315 -0
- package/src/tools/exocom-profile.ts +102 -0
- package/src/tools/exocom.ts +20 -3
- package/src/tools/flow.ts +24 -8
- package/src/tools/intercom-tool.ts +98 -14
- package/src/tools/models.ts +15 -0
- package/src/tools/monitor.ts +20 -1
- package/src/tools/read-result.ts +52 -0
- package/src/tools/timer.ts +23 -2
- package/src/ui/activity-rows.ts +34 -0
- package/src/ui/agent-overlay.ts +23 -13
- package/src/ui/agent-tree.ts +67 -6
- package/src/ui/literal-tool-result.ts +1 -1
- package/src/ui/model-picker.ts +1 -1
- package/src/ui/orchestration-launch.ts +49 -0
- package/src/ui/session-cards.ts +191 -14
- package/src/ui/strategy-leg.ts +40 -0
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
import type { AgentToolUpdateCallback, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import type { AgentConfig } from "../agents/agent.ts";
|
|
4
4
|
import { Type } from "typebox";
|
|
5
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
6
|
+
import { THINKING_LEVELS, isThinkingLevel, type ThinkingLevel } from "../core/types.ts";
|
|
7
|
+
import { validateParallelWriteSets } from "../core/ownership.ts";
|
|
8
|
+
import { effortFullName } from "../core/leg-thinking.ts";
|
|
5
9
|
import { END_TURN_NOTE } from "../core/brief.ts";
|
|
6
10
|
import { fenceUntrusted } from "../core/fence.ts";
|
|
7
11
|
import { Text, visibleWidth } from "@earendil-works/pi-tui";
|
|
@@ -16,7 +20,7 @@ import { expandDetailHint, failureDetails, isInline } from "../extension/shared.
|
|
|
16
20
|
import {
|
|
17
21
|
DelegationLedger, type DelegateParams as DelegateCall, keyInputOf, nameFor, normalizeDelegateConcurrency,
|
|
18
22
|
shortModel, shouldRecordDelegationOutcome, specOf, unknownAgentError, unknownContractError,
|
|
19
|
-
validateDelegationBrief,
|
|
23
|
+
validateDelegationBrief, coerceDelegateParams,
|
|
20
24
|
} from "./delegate.ts";
|
|
21
25
|
import { Semaphore } from "../orchestration/parallel.ts";
|
|
22
26
|
import type { AgentRunSpec, SteerFn, StrategyEngine } from "../orchestration/sdk.ts";
|
|
@@ -24,7 +28,7 @@ import type { AgentResult } from "../orchestration/types.ts";
|
|
|
24
28
|
import { sanitizeLabel } from "../extension/shared.ts";
|
|
25
29
|
import type { PersonaController } from "../persona/controller.ts";
|
|
26
30
|
import type { AgentTree } from "../ui/agent-tree.ts";
|
|
27
|
-
import { buildCompletionReport, runDisplayName, type AsyncRunTracker } from "../engine/async.ts";
|
|
31
|
+
import { buildCompletionReport, observedRunModel, runDisplayName, type AsyncRunTracker } from "../engine/async.ts";
|
|
28
32
|
import type { BackgroundRuns } from "./background-run.ts";
|
|
29
33
|
import { emptyUsage, type ProgressSnapshot, type ToolEvent } from "../engine/stream.ts";
|
|
30
34
|
import { progressPatch } from "../ui/agent-tree.ts";
|
|
@@ -35,6 +39,8 @@ interface DelegateLaunchSnapshot {
|
|
|
35
39
|
label: string;
|
|
36
40
|
agent: string;
|
|
37
41
|
model?: string;
|
|
42
|
+
/** Effort THIS call declared in its args (spec-level); absent when the call declared none. */
|
|
43
|
+
thinking?: ThinkingLevel;
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
interface DelegateLaunchRow {
|
|
@@ -43,6 +49,7 @@ interface DelegateLaunchRow {
|
|
|
43
49
|
detailLabel: string;
|
|
44
50
|
agent: string;
|
|
45
51
|
model?: string;
|
|
52
|
+
thinking?: ThinkingLevel;
|
|
46
53
|
displayLabel: string;
|
|
47
54
|
clipped: boolean;
|
|
48
55
|
hasAlias: boolean;
|
|
@@ -57,6 +64,12 @@ interface CollapsedLaunchLine {
|
|
|
57
64
|
|
|
58
65
|
const COLLAPSED_LAUNCH_ROWS = 3;
|
|
59
66
|
|
|
67
|
+
/** One formatter for every launch-card metadata line: agent · model · FULL effort name —
|
|
68
|
+
* only the effort this call declared; `off` renders nothing (absence means off). */
|
|
69
|
+
function launchRowMetadata(row: { agent?: string; model?: string; thinking?: ThinkingLevel }): string {
|
|
70
|
+
return [row.agent, row.model, row.thinking ? effortFullName(row.thinking) : ""].filter((value): value is string => !!value).join(" · ");
|
|
71
|
+
}
|
|
72
|
+
|
|
60
73
|
/** Prefer session-persisted identity, then recover old launch cards from the live tracker. */
|
|
61
74
|
function launchRows(
|
|
62
75
|
details: { runId?: unknown; runIds?: unknown; runs?: unknown },
|
|
@@ -74,6 +87,7 @@ function launchRows(
|
|
|
74
87
|
label: run.label,
|
|
75
88
|
agent: run.agent,
|
|
76
89
|
...(typeof run.model === "string" ? { model: run.model } : {}),
|
|
90
|
+
...(isThinkingLevel(run.thinking) ? { thinking: run.thinking } : {}),
|
|
77
91
|
});
|
|
78
92
|
}
|
|
79
93
|
}
|
|
@@ -111,6 +125,7 @@ function launchRows(
|
|
|
111
125
|
label: sanitizeTerminalText(run.label),
|
|
112
126
|
agent: sanitizeTerminalText(run.agent),
|
|
113
127
|
...(run.model ? { model: sanitizeTerminalText(run.model) } : {}),
|
|
128
|
+
...(run.thinking ? { thinking: run.thinking } : {}),
|
|
114
129
|
fullName: runDisplayName(run),
|
|
115
130
|
hasAlias,
|
|
116
131
|
}));
|
|
@@ -120,7 +135,7 @@ function launchRows(
|
|
|
120
135
|
const duplicateSuffix = counts.get(row.fullName)! > 1 ? ` [${compactInlineText(row.id, { maxChars: 16 })}]` : "";
|
|
121
136
|
const detailLabel = `${row.label}${duplicateSuffix}`;
|
|
122
137
|
const displayLabel = compactInlineText(detailLabel, { maxChars: 80 });
|
|
123
|
-
const displayMetadata =
|
|
138
|
+
const displayMetadata = launchRowMetadata(row);
|
|
124
139
|
return {
|
|
125
140
|
...row,
|
|
126
141
|
detailLabel,
|
|
@@ -141,7 +156,7 @@ function collapsedLaunchRows(rows: readonly DelegateLaunchRow[], dropped: number
|
|
|
141
156
|
];
|
|
142
157
|
const hintText = compactInlineText(expandDetailHint(), { maxChars: 32 });
|
|
143
158
|
const rowsNeedHint = rows.slice(0, visibleCount).some((row, index) => {
|
|
144
|
-
const metadata =
|
|
159
|
+
const metadata = launchRowMetadata(row);
|
|
145
160
|
const prefixWidth = index === 0 ? visibleWidth("launched ") : visibleWidth(" ");
|
|
146
161
|
return row.clipped || prefixWidth + visibleWidth(row.displayLabel) + (metadata ? 3 + visibleWidth(metadata) : 0) > MAX_COLLAPSED_CARD_COLUMNS;
|
|
147
162
|
});
|
|
@@ -151,7 +166,7 @@ function collapsedLaunchRows(rows: readonly DelegateLaunchRow[], dropped: number
|
|
|
151
166
|
const prefix = index === 0 ? "launched " : " ";
|
|
152
167
|
const fixedWidth = visibleWidth(prefix) + (index === visibleCount - 1 ? visibleWidth(suffix) : 0);
|
|
153
168
|
const budget = Math.max(16, MAX_COLLAPSED_CARD_COLUMNS - fixedWidth);
|
|
154
|
-
const metadata =
|
|
169
|
+
const metadata = launchRowMetadata(row);
|
|
155
170
|
const metadataBudget = Math.min(36, visibleWidth(metadata));
|
|
156
171
|
const labelBudget = Math.max(16, budget - (metadata ? metadataBudget + 3 : 0));
|
|
157
172
|
const label = compactInlineText(row.displayLabel, { maxChars: labelBudget });
|
|
@@ -167,7 +182,7 @@ function collapsedLaunchRows(rows: readonly DelegateLaunchRow[], dropped: number
|
|
|
167
182
|
|
|
168
183
|
function expandedLaunchDetails(text: string, rows: readonly DelegateLaunchRow[]): string {
|
|
169
184
|
if (rows.length === 0 || !rows.some((row) => row.hasAlias)) return text;
|
|
170
|
-
const diagnostics = rows.map((row) => `- ${[row.detailLabel, row.agent, row.model].filter((value): value is string => !!value).join(" · ")} [${row.id}]`).join("\n");
|
|
185
|
+
const diagnostics = rows.map((row) => `- ${[row.detailLabel, row.agent, row.model, row.thinking ? effortFullName(row.thinking) : ""].filter((value): value is string => !!value).join(" · ")} [${row.id}]`).join("\n");
|
|
171
186
|
return `${text}\n\nLaunched workers:\n${diagnostics}`;
|
|
172
187
|
}
|
|
173
188
|
|
|
@@ -190,7 +205,7 @@ function singleLaunchReceipt(row: DelegateLaunchRow): string[] {
|
|
|
190
205
|
const labelBudget = Math.max(16, MAX_COLLAPSED_CARD_COLUMNS - visibleWidth(prefix) - visibleWidth(suffix));
|
|
191
206
|
const label = compactInlineText(row.displayLabel, { maxChars: labelBudget });
|
|
192
207
|
const rows = [compactVisibleLine(`${prefix}${label}${suffix}`, MAX_COLLAPSED_CARD_COLUMNS)];
|
|
193
|
-
const metadata =
|
|
208
|
+
const metadata = launchRowMetadata(row);
|
|
194
209
|
if (metadata) rows.push(compactVisibleLine(` ${compactInlineText(metadata, { maxChars: MAX_COLLAPSED_CARD_COLUMNS - 2 })}`, MAX_COLLAPSED_CARD_COLUMNS));
|
|
195
210
|
return rows;
|
|
196
211
|
}
|
|
@@ -222,6 +237,9 @@ export function registerDelegateTool(pi: ExtensionAPI, d: DelegateToolDeps): voi
|
|
|
222
237
|
const JsonString = Type.String({
|
|
223
238
|
description: "JSON text of the same value — accepted when a client stringifies nested args",
|
|
224
239
|
});
|
|
240
|
+
const ThinkingSchema = StringEnum(THINKING_LEVELS, {
|
|
241
|
+
description: "Requested effort for this leg (off, minimal, low, medium, high, xhigh, max). Overrides the agent and shared child defaults; Pi clamps to the selected model's capabilities. Results distinguish requested from observed Pi effort, not provider cognition.",
|
|
242
|
+
});
|
|
225
243
|
const SkillsSchema = Type.Array(Type.String(), {
|
|
226
244
|
description: "Skills the sub-agent loads first — spawns a dynamic specialist (skills are inherited from the host)",
|
|
227
245
|
});
|
|
@@ -253,6 +271,7 @@ export function registerDelegateTool(pi: ExtensionAPI, d: DelegateToolDeps): voi
|
|
|
253
271
|
),
|
|
254
272
|
skills: Type.Optional(Type.Union([SkillsSchema, JsonString])),
|
|
255
273
|
role: Type.Optional(RoleSchema),
|
|
274
|
+
thinking: Type.Optional(ThinkingSchema),
|
|
256
275
|
model: Type.Optional(
|
|
257
276
|
Type.String({ description: "Optional exact provider/id. Prefer cached nearby family/generation candidates; use `models` for targeted lookup or refresh only when needed. Omission retains saved/agent selections and defaults; an explicit current supervisor provider/id selects it deliberately, maintaining provider/billing authorization. Explicit user selections win." }),
|
|
258
277
|
),
|
|
@@ -278,6 +297,7 @@ export function registerDelegateTool(pi: ExtensionAPI, d: DelegateToolDeps): voi
|
|
|
278
297
|
name: Type.Optional(Type.String({ description: LeaderNameDescription })),
|
|
279
298
|
skills: Type.Optional(Type.Union([SkillsSchema, JsonString])),
|
|
280
299
|
role: Type.Optional(RoleSchema),
|
|
300
|
+
thinking: Type.Optional(ThinkingSchema),
|
|
281
301
|
model: Type.Optional(Type.String({ description: "Optional exact provider/id (single mode). Prefer cached nearby family/generation candidates; use `models` for targeted lookup or refresh only when needed. Omission retains saved/agent selections and defaults; an explicit current supervisor provider/id selects it deliberately, maintaining provider/billing authorization. Explicit user selections win." })),
|
|
282
302
|
tools: Type.Optional(Type.Union([Type.Array(Type.String(), { description: "Tool allowlist override (single mode); [] explicitly grants no tools" }), JsonString])),
|
|
283
303
|
isolation: Type.Optional(
|
|
@@ -476,6 +496,10 @@ export function registerDelegateTool(pi: ExtensionAPI, d: DelegateToolDeps): voi
|
|
|
476
496
|
kind: "subagent",
|
|
477
497
|
agent,
|
|
478
498
|
...(runSpec.model ? { model: runSpec.model } : {}),
|
|
499
|
+
// The requested effort rides the SAME launch seam as the model above — spec-level, what
|
|
500
|
+
// the launch row carried. Rendered as the compact sigla after the model; the drill-in
|
|
501
|
+
// uses full names. Absent ⇒ nothing. Nothing observed is claimed here.
|
|
502
|
+
...(runSpec.thinking ? { thinking: runSpec.thinking } : {}),
|
|
479
503
|
detail: "queued",
|
|
480
504
|
});
|
|
481
505
|
// Arm the async-run status/stall monitor (no-op if both cadences are disabled). A headless leg runs
|
|
@@ -494,9 +518,14 @@ export function registerDelegateTool(pi: ExtensionAPI, d: DelegateToolDeps): voi
|
|
|
494
518
|
const dropped = droppedNote && `
|
|
495
519
|
|
|
496
520
|
[pi-persona]${droppedNote}`;
|
|
521
|
+
const settledRuns = runs.map((run) => {
|
|
522
|
+
const modelUsed = observedRunModel(run);
|
|
523
|
+
return { id: run.id, agent: run.agent, ...(run.label ? { label: run.label } : {}),
|
|
524
|
+
...(run.model ? { requestedModel: run.model } : {}), ...(modelUsed ? { modelUsed } : {}) };
|
|
525
|
+
});
|
|
497
526
|
return {
|
|
498
527
|
content: [{ type: "text" as const, text: `${buildCompletionReport(runs, fenceUntrusted, { bounded: false })}${dropped}${d.drainBusBlock()}` }],
|
|
499
|
-
details: ok ? {} : failureDetails({}),
|
|
528
|
+
details: ok ? { settledRuns } : failureDetails({ settledRuns }),
|
|
500
529
|
isError: !ok,
|
|
501
530
|
...toolUsageField(usage),
|
|
502
531
|
};
|
|
@@ -513,7 +542,7 @@ export function registerDelegateTool(pi: ExtensionAPI, d: DelegateToolDeps): voi
|
|
|
513
542
|
"No fitting agent? Shape one on the fly: `operator` + `role` (extra system prompt) + `skills`.",
|
|
514
543
|
"Assign each worker's name before launch so it sees the same identity from its first turn; omitted names keep the generic fallback.",
|
|
515
544
|
"A `model` may be a loose name ('sonnet', 'opus-4.6', 'claude-pro-max-native/opus-4-6') — it resolves to YOUR provider's id; ambiguous names return",
|
|
516
|
-
"candidates. Other options: name, tools, outputContract, writeSet, isolation: \"worktree\", mcp, concurrency, tasks[].timeoutMs.",
|
|
545
|
+
"candidates. Other options: name, thinking (per-leg effort; Pi clamps), tools, outputContract, writeSet, isolation: \"worktree\", mcp, concurrency, tasks[].timeoutMs.",
|
|
517
546
|
].join(" "),
|
|
518
547
|
parameters: DelegateParams,
|
|
519
548
|
async execute(_toolCallId, rawParams, signal, onUpdate, ctx) {
|
|
@@ -665,7 +694,7 @@ export function registerDelegateTool(pi: ExtensionAPI, d: DelegateToolDeps): voi
|
|
|
665
694
|
const label = nameFor(t, nameOffset + i);
|
|
666
695
|
const id = launchAsyncRun(t.agent, t.task, spec, label, inline, fanOut);
|
|
667
696
|
const model = shortModel(spec.model);
|
|
668
|
-
return { id, label, agent: t.agent, ...(model ? { model } : {}) };
|
|
697
|
+
return { id, label, agent: t.agent, ...(model ? { model } : {}), ...(spec.thinking ? { thinking: spec.thinking } : {}) };
|
|
669
698
|
});
|
|
670
699
|
const ids = runs.map((run) => run.id);
|
|
671
700
|
const droppedNote = dropped > 0 ? ` ${dropped} task(s) beyond the max-children limit (${d.RUN_LIMITS.maxChildren}) were dropped.` : "";
|
|
@@ -700,7 +729,7 @@ export function registerDelegateTool(pi: ExtensionAPI, d: DelegateToolDeps): voi
|
|
|
700
729
|
text: `Launched async run ${id} (${agent}) — runs in the background and wakes you on completion (${END_TURN_NOTE}).`,
|
|
701
730
|
},
|
|
702
731
|
],
|
|
703
|
-
details: { runId: id, runs: [{ id, label, agent, ...(model ? { model } : {}) }] },
|
|
732
|
+
details: { runId: id, runs: [{ id, label, agent, ...(model ? { model } : {}), ...(runSpec.thinking ? { thinking: runSpec.thinking } : {}) }] },
|
|
704
733
|
isError: false,
|
|
705
734
|
};
|
|
706
735
|
}
|
|
@@ -716,8 +745,13 @@ export function registerDelegateTool(pi: ExtensionAPI, d: DelegateToolDeps): voi
|
|
|
716
745
|
const coerced = coerceDelegateParams(args);
|
|
717
746
|
const view = coerced.ok ? coerced.params : undefined;
|
|
718
747
|
if (view?.tasks && view.tasks.length > 0) {
|
|
719
|
-
// Names live in the tree / final card — keep the call line itself minimal.
|
|
720
|
-
|
|
748
|
+
// Names live in the tree / final card — keep the call line itself minimal. A UNIFORM
|
|
749
|
+
// declared effort rides along (every leg declares the same level; mixed or undeclared
|
|
750
|
+
// stays silent — no re-parsing of stringified tasks); per-leg rows carry their own.
|
|
751
|
+
const first = view.tasks[0]!;
|
|
752
|
+
const uniform = view.tasks.every((task) => task.thinking === first.thinking) ? first.thinking : undefined;
|
|
753
|
+
const effort = uniform ? effortFullName(uniform) : "";
|
|
754
|
+
return new Text(`${title}${theme.fg("accent", `parallel (${view.tasks.length})${effort ? ` · ${effort}` : ""}`)}`, 0, 0);
|
|
721
755
|
}
|
|
722
756
|
if (typeof args.tasks === "string" && args.tasks.trim()) {
|
|
723
757
|
return new Text(`${title}${theme.fg("accent", "parallel")}`, 0, 0);
|
|
@@ -728,7 +762,9 @@ export function registerDelegateTool(pi: ExtensionAPI, d: DelegateToolDeps): voi
|
|
|
728
762
|
const agent = compactInlineText(view?.agent ?? (typeof args.agent === "string" ? args.agent : ""), { maxChars: 40 });
|
|
729
763
|
const model = typeof modelValue === "string" && modelValue.trim() ? compactInlineText(modelValue, { maxChars: 80 }) : "";
|
|
730
764
|
const titleLine = `${title}${theme.fg("accent", theme.bold(compactVisibleLine(alias, MAX_COLLAPSED_CARD_COLUMNS - visibleWidth("delegate "))))}`;
|
|
731
|
-
|
|
765
|
+
// Only the effort THIS call's args declare — never a value inherited from agent config.
|
|
766
|
+
const thinking = view?.thinking ?? (isThinkingLevel(args.thinking) ? args.thinking : undefined);
|
|
767
|
+
const metadata = [agent, model, thinking ? effortFullName(thinking) : ""].filter(Boolean).join(" · ");
|
|
732
768
|
const metadataLine = metadata ? theme.fg("dim", compactVisibleLine(` ${metadata}`, MAX_COLLAPSED_CARD_COLUMNS)) : "";
|
|
733
769
|
return new Text([titleLine, metadataLine].filter(Boolean).join("\n"), 0, 0);
|
|
734
770
|
},
|
package/src/tools/delegate.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { cappedList } from "../core/format.ts";
|
|
9
|
+
import { isThinkingLevel, THINKING_LEVELS, type ThinkingLevel } from "../core/types.ts";
|
|
9
10
|
import { sanitizeDisplayLabel } from "../core/display-label.ts";
|
|
10
11
|
import { type AgentRunSpec, type InfrastructureFailure, isInfrastructureFailure, isPositiveFiniteMs } from "../orchestration/sdk.ts";
|
|
11
12
|
|
|
@@ -20,6 +21,8 @@ export interface DelegateTask {
|
|
|
20
21
|
skills?: string[];
|
|
21
22
|
/** Model override for this sub-agent. */
|
|
22
23
|
model?: string;
|
|
24
|
+
/** Requested effort for this leg; overrides agent and shared child defaults. Pi clamps it. */
|
|
25
|
+
thinking?: ThinkingLevel;
|
|
23
26
|
/** Tool allowlist override for this sub-agent. */
|
|
24
27
|
tools?: string[];
|
|
25
28
|
/** Extra system-prompt text: an ON-THE-FLY specialist persona, appended to the
|
|
@@ -47,6 +50,7 @@ export interface DelegateParams {
|
|
|
47
50
|
name?: string;
|
|
48
51
|
skills?: string[];
|
|
49
52
|
model?: string;
|
|
53
|
+
thinking?: ThinkingLevel;
|
|
50
54
|
tools?: string[];
|
|
51
55
|
role?: string;
|
|
52
56
|
isolation?: "none" | "worktree";
|
|
@@ -133,10 +137,6 @@ export function validateDelegationBrief(params: DelegateParams): string | undefi
|
|
|
133
137
|
return undefined;
|
|
134
138
|
}
|
|
135
139
|
|
|
136
|
-
// Moved to core/ownership.ts (so a strategy can call it without importing a tool module);
|
|
137
|
-
// re-exported here unchanged for this file's own call sites and existing tests.
|
|
138
|
-
export { findWriteSetOverlaps, validateParallelWriteSets, type WriteSetOverlap } from "../core/ownership.ts";
|
|
139
|
-
|
|
140
140
|
function unwrapJsonish(value: unknown, label: string): { ok: true; value: unknown } | { ok: false; error: string } {
|
|
141
141
|
let current = value;
|
|
142
142
|
for (let depth = 0; depth < 2; depth++) {
|
|
@@ -205,6 +205,9 @@ function decodedTaskFieldError(bag: Record<string, unknown>, index: number): str
|
|
|
205
205
|
return `delegate: ${label}.${field} must be a string.`;
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
|
+
if (bag.thinking !== undefined && !isThinkingLevel(bag.thinking)) {
|
|
209
|
+
return `delegate: ${label}.thinking must be one of: ${THINKING_LEVELS.join(", ")}.`;
|
|
210
|
+
}
|
|
208
211
|
if (bag.isolation !== undefined && bag.isolation !== "none" && bag.isolation !== "worktree") {
|
|
209
212
|
return `delegate: ${label}.isolation must be "none" or "worktree".`;
|
|
210
213
|
}
|
|
@@ -264,6 +267,9 @@ export function coerceDelegateParams(raw: unknown): { ok: true; params: Delegate
|
|
|
264
267
|
return { ok: false, error: "delegate: arguments must be an object." };
|
|
265
268
|
}
|
|
266
269
|
const bag = { ...(raw as Record<string, unknown>) };
|
|
270
|
+
if (bag.thinking !== undefined && !isThinkingLevel(bag.thinking)) {
|
|
271
|
+
return { ok: false, error: `delegate: thinking must be one of: ${THINKING_LEVELS.join(", ")}.` };
|
|
272
|
+
}
|
|
267
273
|
const tasks = coerceTaskList(bag.tasks);
|
|
268
274
|
if (!tasks.ok) return tasks;
|
|
269
275
|
if (tasks.value !== undefined) bag.tasks = tasks.value;
|
|
@@ -282,10 +288,10 @@ export function coerceDelegateParams(raw: unknown): { ok: true; params: Delegate
|
|
|
282
288
|
return { ok: true, params: bag as DelegateParams };
|
|
283
289
|
}
|
|
284
290
|
|
|
285
|
-
/** Short model name for display: drop provider + `claude
|
|
291
|
+
/** Short model name for display: drop provider + `claude-`; model ids remain opaque. */
|
|
286
292
|
export function shortModel(ref: string | undefined): string {
|
|
287
293
|
if (!ref) return "";
|
|
288
|
-
const base = ref.split("
|
|
294
|
+
const base = ref.split("/").pop() ?? ref;
|
|
289
295
|
return base.replace(/^claude-/, "");
|
|
290
296
|
}
|
|
291
297
|
|
|
@@ -321,6 +327,7 @@ export function specOf(t: DelegateTask, index = 0): AgentRunSpec {
|
|
|
321
327
|
};
|
|
322
328
|
if (t.skills && t.skills.length > 0) spec.skills = t.skills;
|
|
323
329
|
if (t.model) spec.model = t.model;
|
|
330
|
+
if (t.thinking !== undefined) spec.thinking = t.thinking;
|
|
324
331
|
if (t.tools !== undefined) spec.tools = t.tools;
|
|
325
332
|
if (t.role?.trim()) spec.role = t.role.trim();
|
|
326
333
|
if (t.outputContract?.trim()) spec.outputContract = t.outputContract.trim();
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/** Runtime validation and TypeBox schemas for the Exocom collaboration tools. */
|
|
2
|
+
import { type Static, Type } from "typebox";
|
|
3
|
+
|
|
4
|
+
import { writeSetPathError } from "../core/ownership.ts";
|
|
5
|
+
|
|
6
|
+
const TOKEN = { minLength: 1, maxLength: 128, pattern: "^[A-Za-z0-9._:-]+$" } as const;
|
|
7
|
+
export const TaskParams = Type.Object({
|
|
8
|
+
action: Type.Union([Type.Literal("create"), Type.Literal("take"), Type.Literal("progress"), Type.Literal("finish"), Type.Literal("list"), Type.Literal("get")], {
|
|
9
|
+
description: "Choose list to discover tasks (do not pass work_key or task_id); use get with both identifiers returned by create/list to inspect one exact task generation.",
|
|
10
|
+
}),
|
|
11
|
+
work_key: Type.Optional(Type.String({ ...TOKEN, description: "Required for create, take, progress, finish, and get. Omit for list. Pair with task_id for an existing task." })),
|
|
12
|
+
task_id: Type.Optional(Type.String({ ...TOKEN, description: "Generation id returned by create or list. Required with work_key for take, progress, finish, and get; omit for create and list." })),
|
|
13
|
+
description: Type.Optional(Type.String({ minLength: 1, maxLength: 2_048 })),
|
|
14
|
+
write_set: Type.Optional(Type.Array(Type.String({ minLength: 1, maxLength: 256 }), { maxItems: 64 })),
|
|
15
|
+
note: Type.Optional(Type.String({ minLength: 1, maxLength: 4_096 })),
|
|
16
|
+
outcome: Type.Optional(Type.Union([Type.Literal("done"), Type.Literal("abandoned")])),
|
|
17
|
+
offset: Type.Optional(Type.Integer({ minimum: 0, maximum: 64 })),
|
|
18
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 20, description: "Rows per page: defaults to 10 for summaries or 5 when detail=true. Detailed pages allow at most 5." })),
|
|
19
|
+
detail: Type.Optional(Type.Boolean({ description: "Include peer-authored task/progress prose. Detailed lists default to 5 rows and reject an explicit limit above 5." })),
|
|
20
|
+
});
|
|
21
|
+
export const OfferParams = Type.Object({
|
|
22
|
+
work_key: Type.String({ ...TOKEN }),
|
|
23
|
+
task_id: Type.String({ ...TOKEN }),
|
|
24
|
+
target: Type.Optional(Type.String({ minLength: 1, maxLength: 80 })),
|
|
25
|
+
expiresInMinutes: Type.Optional(Type.Integer({ minimum: 1, maximum: 1_440 })),
|
|
26
|
+
});
|
|
27
|
+
export const PublishParams = Type.Object({
|
|
28
|
+
action: Type.Union([Type.Literal("publish"), Type.Literal("read")]),
|
|
29
|
+
work_key: Type.Optional(Type.String({ ...TOKEN })),
|
|
30
|
+
task_id: Type.Optional(Type.String({ ...TOKEN })),
|
|
31
|
+
title: Type.Optional(Type.String({ minLength: 1, maxLength: 160 })),
|
|
32
|
+
excerpt: Type.Optional(Type.String({ minLength: 1, maxLength: 4_096 })),
|
|
33
|
+
msg_id: Type.Optional(Type.String({ ...TOKEN })),
|
|
34
|
+
offset: Type.Optional(Type.Integer({ minimum: 0, maximum: 64 })),
|
|
35
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 20 })),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
type RecordValue = Record<string, unknown>;
|
|
39
|
+
const isRecord = (value: unknown): value is RecordValue => !!value && typeof value === "object" && !Array.isArray(value);
|
|
40
|
+
const isToken = (value: unknown): value is string => typeof value === "string" && /^[A-Za-z0-9._:-]{1,128}$/.test(value);
|
|
41
|
+
function invalid(tool: string, field: string, expected: string): never {
|
|
42
|
+
throw new Error(`${tool}: ${field} ${expected}`);
|
|
43
|
+
}
|
|
44
|
+
function exactKeys(tool: string, value: RecordValue, allowed: readonly string[]): void {
|
|
45
|
+
const unexpected = Object.keys(value).find((key) => !allowed.includes(key));
|
|
46
|
+
if (unexpected) invalid(tool, unexpected, "is not a supported parameter");
|
|
47
|
+
}
|
|
48
|
+
function boundedString(tool: string, field: string, value: unknown, max: number): string {
|
|
49
|
+
if (typeof value !== "string" || value.length < 1 || value.length > max) invalid(tool, field, `must be 1–${max} characters`);
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function tokenField(tool: string, field: string, value: unknown): string {
|
|
53
|
+
if (!isToken(value)) invalid(tool, field, "must be 1–128 token characters matching [A-Za-z0-9._:-]");
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
function objectInput(tool: string, input: unknown): RecordValue {
|
|
57
|
+
if (!isRecord(input)) invalid(tool, "parameters", "must be an object");
|
|
58
|
+
return input;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type TaskInput =
|
|
62
|
+
| { action: "list"; offset: number; limit: number; detail: boolean }
|
|
63
|
+
| { action: "get"; work_key: string; task_id: string }
|
|
64
|
+
| { action: "create"; work_key: string; description: string; write_set: string[] }
|
|
65
|
+
| { action: "take"; work_key: string; task_id: string }
|
|
66
|
+
| { action: "progress"; work_key: string; task_id: string; note: string }
|
|
67
|
+
| { action: "finish"; work_key: string; task_id: string; note: string; outcome: "done" | "abandoned" };
|
|
68
|
+
|
|
69
|
+
export function parseTaskInput(input: unknown): TaskInput {
|
|
70
|
+
const tool = "exocom_task";
|
|
71
|
+
const value = objectInput(tool, input);
|
|
72
|
+
if (typeof value.action !== "string" || !["create", "take", "progress", "finish", "list", "get"].includes(value.action)) invalid(tool, "action", "must be create, take, progress, finish, list, or get");
|
|
73
|
+
const action = value.action as "create" | "take" | "progress" | "finish" | "list" | "get";
|
|
74
|
+
if (action === "list") {
|
|
75
|
+
exactKeys(tool, value, ["action", "offset", "limit", "detail"]);
|
|
76
|
+
const offset = value.offset === undefined ? 0 : value.offset;
|
|
77
|
+
const detail = value.detail === undefined ? false : value.detail;
|
|
78
|
+
const limit = value.limit === undefined ? (detail ? 5 : 10) : value.limit;
|
|
79
|
+
if (!Number.isInteger(offset) || (offset as number) < 0 || (offset as number) > 64) invalid(tool, "offset", "must be an integer from 0 to 64");
|
|
80
|
+
if (!Number.isInteger(limit) || (limit as number) < 1 || (limit as number) > 20) invalid(tool, "limit", "must be an integer from 1 to 20");
|
|
81
|
+
if (typeof detail !== "boolean") invalid(tool, "detail", "must be a boolean");
|
|
82
|
+
return { action, offset: offset as number, limit: limit as number, detail };
|
|
83
|
+
}
|
|
84
|
+
if (action === "get") {
|
|
85
|
+
exactKeys(tool, value, ["action", "work_key", "task_id"]);
|
|
86
|
+
return { action, work_key: tokenField(tool, "work_key", value.work_key), task_id: tokenField(tool, "task_id", value.task_id) };
|
|
87
|
+
}
|
|
88
|
+
if (action === "create") {
|
|
89
|
+
exactKeys(tool, value, ["action", "work_key", "description", "write_set"]);
|
|
90
|
+
const work_key = tokenField(tool, "work_key", value.work_key);
|
|
91
|
+
const description = boundedString(tool, "description", value.description, 2_048);
|
|
92
|
+
const write_set = value.write_set === undefined ? [] : value.write_set;
|
|
93
|
+
if (!Array.isArray(write_set) || write_set.length > 64 || write_set.some((path) => typeof path !== "string" || path.length < 1 || path.length > 256)) {
|
|
94
|
+
invalid(tool, "write_set", "must contain at most 64 paths of 1–256 characters");
|
|
95
|
+
}
|
|
96
|
+
for (const path of write_set as string[]) {
|
|
97
|
+
const issue = writeSetPathError(path);
|
|
98
|
+
if (issue) invalid(tool, "write_set path", `is invalid (${issue})`);
|
|
99
|
+
}
|
|
100
|
+
return { action, work_key, description, write_set: write_set as string[] };
|
|
101
|
+
}
|
|
102
|
+
const work_key = tokenField(tool, "work_key", value.work_key);
|
|
103
|
+
if (action === "take") {
|
|
104
|
+
exactKeys(tool, value, ["action", "work_key", "task_id"]);
|
|
105
|
+
return { action, work_key, task_id: tokenField(tool, "task_id", value.task_id) };
|
|
106
|
+
}
|
|
107
|
+
if (action === "progress") {
|
|
108
|
+
exactKeys(tool, value, ["action", "work_key", "task_id", "note"]);
|
|
109
|
+
return { action, work_key, task_id: tokenField(tool, "task_id", value.task_id), note: boundedString(tool, "note", value.note, 4_096) };
|
|
110
|
+
}
|
|
111
|
+
exactKeys(tool, value, ["action", "work_key", "task_id", "note", "outcome"]);
|
|
112
|
+
if (value.outcome !== "done" && value.outcome !== "abandoned") invalid(tool, "outcome", "must be done or abandoned");
|
|
113
|
+
return { action, work_key, task_id: tokenField(tool, "task_id", value.task_id), note: boundedString(tool, "note", value.note, 4_096), outcome: value.outcome };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function parseOfferInput(input: unknown) {
|
|
117
|
+
const value = objectInput("exocom_offer", input);
|
|
118
|
+
exactKeys("exocom_offer", value, ["work_key", "task_id", "target", "expiresInMinutes"]);
|
|
119
|
+
const work_key = tokenField("exocom_offer", "work_key", value.work_key);
|
|
120
|
+
const task_id = tokenField("exocom_offer", "task_id", value.task_id);
|
|
121
|
+
let target: string | undefined;
|
|
122
|
+
if (value.target !== undefined) {
|
|
123
|
+
const raw = boundedString("exocom_offer", "target", value.target, 80);
|
|
124
|
+
if (raw === "*") invalid("exocom_offer", "target", "must name one peer, never *");
|
|
125
|
+
if (!/^[^\p{Cc}\p{Zl}\p{Zp}<>]+$/u.test(raw) || raw !== raw.trim()) invalid("exocom_offer", "target", "must exactly match a clean public target from exocom_list");
|
|
126
|
+
target = raw;
|
|
127
|
+
}
|
|
128
|
+
const expiresInMinutes = value.expiresInMinutes === undefined ? 60 : value.expiresInMinutes;
|
|
129
|
+
if (!Number.isInteger(expiresInMinutes) || (expiresInMinutes as number) < 1 || (expiresInMinutes as number) > 1_440) {
|
|
130
|
+
invalid("exocom_offer", "expiresInMinutes", "must be an integer from 1 to 1440");
|
|
131
|
+
}
|
|
132
|
+
return { work_key, task_id, target, expiresInMinutes: expiresInMinutes as number };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export type PublishInput =
|
|
136
|
+
| { action: "read"; msg_id: string }
|
|
137
|
+
| { action: "read"; work_key: string; task_id?: string; offset: number; limit: number }
|
|
138
|
+
| { action: "publish"; work_key: string; task_id: string; title: string; excerpt: string };
|
|
139
|
+
|
|
140
|
+
export function parsePublishInput(input: unknown): PublishInput {
|
|
141
|
+
const value = objectInput("exocom_publish", input);
|
|
142
|
+
if (value.action === "read") {
|
|
143
|
+
if (value.msg_id !== undefined) {
|
|
144
|
+
exactKeys("exocom_publish", value, ["action", "msg_id"]);
|
|
145
|
+
return { action: "read", msg_id: tokenField("exocom_publish", "msg_id", value.msg_id) };
|
|
146
|
+
}
|
|
147
|
+
exactKeys("exocom_publish", value, ["action", "work_key", "task_id", "offset", "limit"]);
|
|
148
|
+
const work_key = tokenField("exocom_publish", "work_key", value.work_key);
|
|
149
|
+
const task_id = value.task_id === undefined ? undefined : tokenField("exocom_publish", "task_id", value.task_id);
|
|
150
|
+
const offset = value.offset === undefined ? 0 : value.offset;
|
|
151
|
+
const limit = value.limit === undefined ? 10 : value.limit;
|
|
152
|
+
if (!Number.isInteger(offset) || (offset as number) < 0 || (offset as number) > 64) invalid("exocom_publish", "offset", "must be an integer from 0 to 64");
|
|
153
|
+
if (!Number.isInteger(limit) || (limit as number) < 1 || (limit as number) > 20) invalid("exocom_publish", "limit", "must be an integer from 1 to 20");
|
|
154
|
+
return { action: "read", work_key, ...(task_id === undefined ? {} : { task_id }), offset: offset as number, limit: limit as number };
|
|
155
|
+
}
|
|
156
|
+
if (value.action !== "publish") invalid("exocom_publish", "action", "must be publish or read");
|
|
157
|
+
exactKeys("exocom_publish", value, ["action", "work_key", "task_id", "title", "excerpt"]);
|
|
158
|
+
const work_key = tokenField("exocom_publish", "work_key", value.work_key);
|
|
159
|
+
const task_id = tokenField("exocom_publish", "task_id", value.task_id);
|
|
160
|
+
const title = boundedString("exocom_publish", "title", value.title, 160);
|
|
161
|
+
const excerpt = boundedString("exocom_publish", "excerpt", value.excerpt, 4_096);
|
|
162
|
+
if (Buffer.byteLength(excerpt, "utf8") > 8_192) invalid("exocom_publish", "excerpt", "must be at most 8192 UTF-8 bytes");
|
|
163
|
+
return { action: "publish", work_key, task_id, title, excerpt };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export type TaskParamsType = Static<typeof TaskParams>;
|
|
167
|
+
export type OfferParamsType = Static<typeof OfferParams>;
|
|
168
|
+
export type PublishParamsType = Static<typeof PublishParams>;
|