@aeondave/pi-persona 1.17.1 → 1.17.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +103 -64
- package/CHANGELOG.md +310 -1
- package/README.md +37 -23
- package/agents/architect.md +21 -0
- package/agents/balthasar.md +17 -22
- package/agents/casper.md +17 -23
- package/agents/evidence-verifier.md +31 -16
- package/agents/melchior.md +17 -22
- package/agents/operator.md +15 -55
- package/agents/research.md +14 -48
- package/agents/reviewer.md +20 -39
- package/agents/scout.md +14 -15
- package/agents/verifier.md +15 -16
- package/docs/ARCHITECTURE.md +280 -92
- package/docs/COLLABORATION.md +110 -0
- package/docs/EXPERIENCE.md +52 -17
- package/docs/MONITORS.md +62 -3
- package/docs/README.md +12 -0
- package/docs/REFERENCE.md +139 -38
- package/docs/SPINE.md +2 -2
- package/docs/STRATEGIES.md +23 -8
- package/docs/TELEMETRY.md +1 -0
- package/flows/deep-review.flow.json +1 -1
- package/package.json +15 -8
- package/personas/audit.md +28 -17
- package/personas/dev.md +30 -32
- package/personas/elite.md +223 -221
- package/personas/judge.md +25 -19
- package/personas/magi.md +26 -29
- package/personas/planner.md +31 -24
- package/personas/researcher.md +25 -26
- package/personas/swarm.md +35 -25
- package/personas/verify.md +30 -19
- package/src/agents/agent.ts +26 -0
- package/src/bridge.ts +28 -10
- package/src/bus/broker/client.ts +119 -21
- package/src/bus/broker/host.ts +123 -8
- package/src/bus/broker/messages.ts +31 -4
- package/src/bus/contact.ts +113 -38
- package/src/bus/peers.ts +15 -0
- package/src/bus/result-reader.ts +88 -0
- package/src/core/brief.ts +436 -353
- package/src/core/capabilities.ts +1 -1
- package/src/core/config.ts +15 -10
- package/src/core/leg-thinking.ts +57 -0
- package/src/core/model-catalog.ts +171 -0
- package/src/core/model-tier.ts +89 -0
- package/src/core/models.ts +13 -4
- package/src/core/monitor-output.ts +85 -0
- package/src/core/monitor.ts +34 -2
- package/src/core/nudge.ts +176 -23
- package/src/core/ownership.ts +1 -2
- package/src/core/pi-compat.ts +5 -12
- package/src/core/seed.ts +99 -6
- package/src/core/session-runs.ts +87 -0
- package/src/core/settings.ts +1 -1
- package/src/core/timer.ts +34 -9
- package/src/core/types.ts +1 -1
- package/src/engine/adapter.ts +95 -27
- package/src/engine/async.ts +213 -155
- package/src/engine/child.ts +12 -4
- package/src/engine/host-module.ts +12 -0
- package/src/engine/inproc.ts +99 -53
- package/src/engine/result-grant.ts +62 -0
- package/src/engine/session-tools.ts +6 -0
- package/src/engine/stream.ts +10 -20
- 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 +47 -18
- 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/engine.ts +54 -28
- package/src/extension/hooks.ts +952 -868
- package/src/extension/identity.ts +41 -75
- package/src/extension/persona-model-settings.ts +31 -13
- package/src/extension/results.ts +136 -0
- package/src/extension/shared.ts +32 -7
- package/src/extension.ts +2965 -2611
- package/src/loader.ts +11 -3
- package/src/monitor/archive.ts +235 -0
- package/src/monitor/session.ts +21 -1
- package/src/orchestration/flow-journal.ts +33 -6
- package/src/orchestration/flow-run.ts +160 -32
- package/src/orchestration/flow.ts +20 -2
- package/src/orchestration/reducers.ts +9 -5
- package/src/orchestration/render.ts +1 -1
- package/src/orchestration/roster.ts +71 -23
- package/src/orchestration/sdk.ts +61 -11
- package/src/orchestration/strategies/compete.ts +5 -1
- package/src/orchestration/strategies/council-rounds.ts +2 -0
- package/src/orchestration/strategies/critic-loop.ts +3 -0
- package/src/orchestration/strategies/debate.ts +2 -0
- package/src/orchestration/strategies/fanout.ts +3 -2
- package/src/orchestration/strategies/judge.ts +5 -1
- package/src/orchestration/strategies/magi.ts +40 -23
- package/src/orchestration/strategies/map.ts +13 -3
- package/src/orchestration/strategies/pair.ts +2 -0
- package/src/orchestration/strategies/pipeline.ts +2 -0
- package/src/orchestration/strategies/synthesize.ts +27 -7
- package/src/orchestration/strategy.ts +31 -0
- package/src/orchestration/types.ts +10 -0
- package/src/persona/model-participants.ts +20 -7
- package/src/persona/orchestrate.ts +2 -0
- package/src/persona/persona.ts +8 -4
- 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/background-run.ts +151 -0
- package/src/tools/council.ts +108 -66
- package/src/tools/delegate-tool.ts +349 -283
- package/src/tools/delegate.ts +35 -216
- 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-work.ts +18 -53
- package/src/tools/exocom.ts +35 -22
- package/src/tools/flow.ts +103 -30
- package/src/tools/intercom-tool.ts +202 -128
- package/src/tools/intercom.ts +1 -1
- package/src/tools/models.ts +34 -9
- package/src/tools/monitor.ts +58 -6
- package/src/tools/read-result.ts +52 -0
- package/src/tools/timer.ts +34 -15
- package/src/ui/agent-overlay.ts +65 -16
- package/src/ui/agent-tree.ts +127 -19
- package/src/ui/compact-widget.ts +25 -0
- package/src/ui/failure-summary.ts +55 -0
- package/src/ui/literal-tool-result.ts +24 -0
- package/src/ui/model-picker.ts +158 -81
- package/src/ui/orchestration-launch.ts +49 -0
- package/src/ui/persona-settings.ts +9 -3
- package/src/ui/presentation.ts +24 -0
- package/src/ui/report-body.ts +63 -0
- package/src/ui/session-cards.ts +366 -0
- package/src/ui/wake-rows.ts +262 -0
- package/teams.yaml +5 -1
package/src/tools/delegate.ts
CHANGED
|
@@ -1,28 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `delegate` tool's core
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* (e.g. the generic `operator` specialised by the skills it loads). Capability
|
|
7
|
-
* gating happens upstream in the `tool_call` hook.
|
|
2
|
+
* The `delegate` tool's pure core: parameter coercion and validation, the task → run-spec mapping,
|
|
3
|
+
* and the anti-loop ledger. A task may carry `skills`/`model`/`tools` to spawn a *dynamic* sub-agent
|
|
4
|
+
* (e.g. the generic `operator` specialised by the skills it loads). Capability gating happens
|
|
5
|
+
* upstream in the `tool_call` hook; launching and settling live in `delegate-tool.ts`.
|
|
8
6
|
*/
|
|
9
7
|
|
|
10
|
-
import { type ChildUsage, emptyUsage, type ToolEvent } from "../engine/stream.ts";
|
|
11
|
-
import { mapWithConcurrency } from "../orchestration/parallel.ts";
|
|
12
8
|
import { cappedList } from "../core/format.ts";
|
|
9
|
+
import { isThinkingLevel, THINKING_LEVELS, type ThinkingLevel } from "../core/types.ts";
|
|
13
10
|
import { sanitizeDisplayLabel } from "../core/display-label.ts";
|
|
14
|
-
import {
|
|
15
|
-
import { aggregateResults } from "../orchestration/reducers.ts";
|
|
16
|
-
import {
|
|
17
|
-
type AgentRunSpec,
|
|
18
|
-
type InfrastructureFailure,
|
|
19
|
-
isInfrastructureFailure,
|
|
20
|
-
isPositiveFiniteMs,
|
|
21
|
-
legFailure,
|
|
22
|
-
type SteerFn,
|
|
23
|
-
type StrategyEngine,
|
|
24
|
-
} from "../orchestration/sdk.ts";
|
|
25
|
-
import type { AgentResult } from "../orchestration/types.ts";
|
|
11
|
+
import { type AgentRunSpec, type InfrastructureFailure, isInfrastructureFailure, isPositiveFiniteMs } from "../orchestration/sdk.ts";
|
|
26
12
|
|
|
27
13
|
export interface DelegateTask {
|
|
28
14
|
agent: string;
|
|
@@ -35,6 +21,8 @@ export interface DelegateTask {
|
|
|
35
21
|
skills?: string[];
|
|
36
22
|
/** Model override for this sub-agent. */
|
|
37
23
|
model?: string;
|
|
24
|
+
/** Requested effort for this leg; overrides agent and shared child defaults. Pi clamps it. */
|
|
25
|
+
thinking?: ThinkingLevel;
|
|
38
26
|
/** Tool allowlist override for this sub-agent. */
|
|
39
27
|
tools?: string[];
|
|
40
28
|
/** Extra system-prompt text: an ON-THE-FLY specialist persona, appended to the
|
|
@@ -62,6 +50,7 @@ export interface DelegateParams {
|
|
|
62
50
|
name?: string;
|
|
63
51
|
skills?: string[];
|
|
64
52
|
model?: string;
|
|
53
|
+
thinking?: ThinkingLevel;
|
|
65
54
|
tools?: string[];
|
|
66
55
|
role?: string;
|
|
67
56
|
isolation?: "none" | "worktree";
|
|
@@ -73,8 +62,6 @@ export interface DelegateParams {
|
|
|
73
62
|
outputContract?: string;
|
|
74
63
|
tasks?: DelegateTask[];
|
|
75
64
|
concurrency?: number;
|
|
76
|
-
async?: boolean;
|
|
77
|
-
sync?: boolean;
|
|
78
65
|
}
|
|
79
66
|
|
|
80
67
|
/** Structured cold-start context that any persona policy may require. */
|
|
@@ -150,10 +137,6 @@ export function validateDelegationBrief(params: DelegateParams): string | undefi
|
|
|
150
137
|
return undefined;
|
|
151
138
|
}
|
|
152
139
|
|
|
153
|
-
// Moved to core/ownership.ts (so a strategy can call it without importing a tool module);
|
|
154
|
-
// re-exported here unchanged for this file's own call sites and existing tests.
|
|
155
|
-
export { findWriteSetOverlaps, validateParallelWriteSets, type WriteSetOverlap } from "../core/ownership.ts";
|
|
156
|
-
|
|
157
140
|
function unwrapJsonish(value: unknown, label: string): { ok: true; value: unknown } | { ok: false; error: string } {
|
|
158
141
|
let current = value;
|
|
159
142
|
for (let depth = 0; depth < 2; depth++) {
|
|
@@ -222,6 +205,9 @@ function decodedTaskFieldError(bag: Record<string, unknown>, index: number): str
|
|
|
222
205
|
return `delegate: ${label}.${field} must be a string.`;
|
|
223
206
|
}
|
|
224
207
|
}
|
|
208
|
+
if (bag.thinking !== undefined && !isThinkingLevel(bag.thinking)) {
|
|
209
|
+
return `delegate: ${label}.thinking must be one of: ${THINKING_LEVELS.join(", ")}.`;
|
|
210
|
+
}
|
|
225
211
|
if (bag.isolation !== undefined && bag.isolation !== "none" && bag.isolation !== "worktree") {
|
|
226
212
|
return `delegate: ${label}.isolation must be "none" or "worktree".`;
|
|
227
213
|
}
|
|
@@ -281,6 +267,9 @@ export function coerceDelegateParams(raw: unknown): { ok: true; params: Delegate
|
|
|
281
267
|
return { ok: false, error: "delegate: arguments must be an object." };
|
|
282
268
|
}
|
|
283
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
|
+
}
|
|
284
273
|
const tasks = coerceTaskList(bag.tasks);
|
|
285
274
|
if (!tasks.ok) return tasks;
|
|
286
275
|
if (tasks.value !== undefined) bag.tasks = tasks.value;
|
|
@@ -299,32 +288,16 @@ export function coerceDelegateParams(raw: unknown): { ok: true; params: Delegate
|
|
|
299
288
|
return { ok: true, params: bag as DelegateParams };
|
|
300
289
|
}
|
|
301
290
|
|
|
302
|
-
/**
|
|
303
|
-
export interface DelegateView {
|
|
304
|
-
agent: string;
|
|
305
|
-
/** Display label: `name · model` (e.g. "kiln-port · sonnet-4-6"). */
|
|
306
|
-
label: string;
|
|
307
|
-
running: boolean;
|
|
308
|
-
ok: boolean;
|
|
309
|
-
/** Terminal failure classification; `abort` projects as stopped rather than failed in the UI. */
|
|
310
|
-
failureKind?: AgentResult["failureKind"];
|
|
311
|
-
output: string;
|
|
312
|
-
/** The tool the leg is currently running (e.g. "grep src/…"), "" if none. */
|
|
313
|
-
activity: string;
|
|
314
|
-
usage: ChildUsage;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/** Short model name for display: drop provider + `claude-` + any `:thinking`. */
|
|
291
|
+
/** Short model name for display: drop provider + `claude-`; model ids remain opaque. */
|
|
318
292
|
export function shortModel(ref: string | undefined): string {
|
|
319
293
|
if (!ref) return "";
|
|
320
|
-
const base = ref.split("
|
|
294
|
+
const base = ref.split("/").pop() ?? ref;
|
|
321
295
|
return base.replace(/^claude-/, "");
|
|
322
296
|
}
|
|
323
297
|
|
|
324
298
|
/** The bare display NAME for a sub-agent (no model suffix): the loader's name, or a codename
|
|
325
299
|
* for a generic operator, else the agent's own name. Exported so a caller that folds in the
|
|
326
|
-
* model itself (e.g. the async tree node, `AsyncRun.label`)
|
|
327
|
-
* of {@link labelFor}'s composite string. */
|
|
300
|
+
* model itself (e.g. the async tree node, `AsyncRun.label`). */
|
|
328
301
|
export function nameFor(t: { agent: string; name?: string }, index: number): string {
|
|
329
302
|
// No catalog of call-signs: an unnamed generic operator stays the agent name, disambiguated
|
|
330
303
|
// by index. The model is supposed to invent `<call-sign>-<purpose>` itself; this is only the
|
|
@@ -333,28 +306,8 @@ export function nameFor(t: { agent: string; name?: string }, index: number): str
|
|
|
333
306
|
return sanitizeDisplayLabel(t.name?.trim() || fallback, fallback);
|
|
334
307
|
}
|
|
335
308
|
|
|
336
|
-
/**
|
|
337
|
-
|
|
338
|
-
const name = nameFor(t, index);
|
|
339
|
-
const m = shortModel(t.model);
|
|
340
|
-
return m ? `${name} · ${m}` : name;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
export interface DelegateOutcome {
|
|
344
|
-
text: string;
|
|
345
|
-
results: AgentResult[];
|
|
346
|
-
views: DelegateView[];
|
|
347
|
-
ok: boolean;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/** Hard ceilings the engine enforces regardless of what the LLM asks for. */
|
|
351
|
-
export interface DelegateLimits {
|
|
352
|
-
maxConcurrency: number;
|
|
353
|
-
maxChildren: number;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/** Normalize an LLM-authored concurrency request once for sync scheduling, async scheduling, and
|
|
357
|
-
* policy gates. Fractional/non-finite inputs fail toward serialization instead of rounding up. */
|
|
309
|
+
/** Normalize an LLM-authored concurrency request once for scheduling and policy gates.
|
|
310
|
+
* Fractional/non-finite inputs fail toward serialization instead of rounding up. */
|
|
358
311
|
export function normalizeDelegateConcurrency(value: number | undefined, maximum: number): number {
|
|
359
312
|
const ceiling = Math.max(1, Number.isFinite(maximum) ? Math.floor(maximum) : 1);
|
|
360
313
|
if (value === undefined) return ceiling;
|
|
@@ -363,9 +316,8 @@ export function normalizeDelegateConcurrency(value: number | undefined, maximum:
|
|
|
363
316
|
}
|
|
364
317
|
|
|
365
318
|
/** Map a task to the run-spec fields the engine accepts — the single source of truth for the
|
|
366
|
-
* delegate path's field mapping
|
|
367
|
-
*
|
|
368
|
-
* `timeoutMs` could silently miss the async fan-out, which is the interactive default). */
|
|
319
|
+
* delegate path's field mapping (a second copy is how a per-leg knob like `timeoutMs` silently
|
|
320
|
+
* misses one launch route). */
|
|
369
321
|
export function specOf(t: DelegateTask, index = 0): AgentRunSpec {
|
|
370
322
|
const renderedBrief = renderDelegationBrief(t.brief);
|
|
371
323
|
const spec: AgentRunSpec = {
|
|
@@ -375,6 +327,7 @@ export function specOf(t: DelegateTask, index = 0): AgentRunSpec {
|
|
|
375
327
|
};
|
|
376
328
|
if (t.skills && t.skills.length > 0) spec.skills = t.skills;
|
|
377
329
|
if (t.model) spec.model = t.model;
|
|
330
|
+
if (t.thinking !== undefined) spec.thinking = t.thinking;
|
|
378
331
|
if (t.tools !== undefined) spec.tools = t.tools;
|
|
379
332
|
if (t.role?.trim()) spec.role = t.role.trim();
|
|
380
333
|
if (t.outputContract?.trim()) spec.outputContract = t.outputContract.trim();
|
|
@@ -406,6 +359,18 @@ export interface DelegationKeyInput {
|
|
|
406
359
|
isolation?: string;
|
|
407
360
|
}
|
|
408
361
|
|
|
362
|
+
/** The ledger's view of a task or run spec: just the key fields, absent when unset. */
|
|
363
|
+
export function keyInputOf(t: Pick<DelegateTask, "agent" | "task" | "model" | "role" | "tools" | "isolation">): DelegationKeyInput {
|
|
364
|
+
return {
|
|
365
|
+
agent: t.agent,
|
|
366
|
+
task: t.task,
|
|
367
|
+
...(t.model ? { model: t.model } : {}),
|
|
368
|
+
...(t.role ? { role: t.role } : {}),
|
|
369
|
+
...(t.tools !== undefined ? { tools: t.tools } : {}),
|
|
370
|
+
...(t.isolation ? { isolation: t.isolation } : {}),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
409
374
|
export class DelegationLedger {
|
|
410
375
|
private readonly failures = new Map<string, number>();
|
|
411
376
|
private readonly maxKeys = 200;
|
|
@@ -469,9 +434,6 @@ export class DelegationLedger {
|
|
|
469
434
|
* an operator abort is cancellation, and an engine REJECTION is the harness breaking (see
|
|
470
435
|
* {@link isInfrastructureFailure}) — a full disk or a crashed adapter says nothing about whether the
|
|
471
436
|
* delegation was a good idea, and two of them would blacklist a task that was never really attempted.
|
|
472
|
-
* Lives beside the ledger so every path that records feeds it the same notion of evidence: the sync
|
|
473
|
-
* fan-out settles a rejection as `ok:false` while the single and async paths let it propagate, and
|
|
474
|
-
* without this the identical infrastructure crash would count in one path and vanish in the others.
|
|
475
437
|
*/
|
|
476
438
|
export function shouldRecordDelegationOutcome(result: Partial<InfrastructureFailure>): boolean {
|
|
477
439
|
return result.failureKind !== "abort" && !isInfrastructureFailure(result);
|
|
@@ -518,146 +480,3 @@ export function unknownContractError(requested: Array<string | undefined>, insta
|
|
|
518
480
|
"`outputContract` takes a contract NAME; omit it for free-form output and describe the report shape in `requiredArtifacts`."
|
|
519
481
|
);
|
|
520
482
|
}
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* Should this `delegate` call run in the BACKGROUND (async)? An explicit `async` always wins;
|
|
524
|
-
* otherwise an interactive session (`hasUI`) defaults to background — the supervisor stays free and
|
|
525
|
-
* results return as follow-ups — unless the call opts out with `sync: true`. A headless run (no UI,
|
|
526
|
-
* e.g. `pi -p`) defaults to SYNC: the single turn must carry the result, and nothing drains a
|
|
527
|
-
* follow-up after the process exits. `renderCall` passes `hasUI: true` (it only ever fires in a UI),
|
|
528
|
-
* so the async tag it shows tracks the exact same default the execute path takes.
|
|
529
|
-
*/
|
|
530
|
-
export function wantsAsyncRun(params: { async?: boolean; sync?: boolean }, hasUI: boolean): boolean {
|
|
531
|
-
return params.async ?? (hasUI && params.sync !== true);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
function viewOf(agent: string, label: string, r: AgentResult): DelegateView {
|
|
535
|
-
return {
|
|
536
|
-
agent,
|
|
537
|
-
label,
|
|
538
|
-
running: false,
|
|
539
|
-
ok: r.ok,
|
|
540
|
-
...(r.failureKind !== undefined ? { failureKind: r.failureKind } : {}),
|
|
541
|
-
output: r.ok ? r.output || "(no output)" : r.error ?? "(failed)",
|
|
542
|
-
activity: "",
|
|
543
|
-
usage: r.usage,
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
export async function runDelegate(
|
|
548
|
-
params: DelegateParams,
|
|
549
|
-
engine: StrategyEngine,
|
|
550
|
-
limits: DelegateLimits = { maxConcurrency: 4, maxChildren: 8 },
|
|
551
|
-
onProgress?: (views: DelegateView[]) => void,
|
|
552
|
-
onLegStart?: (index: number, abort: () => void) => void,
|
|
553
|
-
onLegSteerable?: (index: number, steer: SteerFn) => void,
|
|
554
|
-
/** The RUN-level abort signal — the same one the caller baked into its engine. A whole-run
|
|
555
|
-
* cancellation reaches a leg through THIS, never through the per-leg controller below, so
|
|
556
|
-
* without it a cancelled leg whose engine rejects is filed as an agent failure and the user's
|
|
557
|
-
* own stop renders as FAILED. Mirrors `SDKDeps.signal`, which classifies the same way. */
|
|
558
|
-
signal?: AbortSignal,
|
|
559
|
-
/** Authoritative per-leg tool lifecycle; arguments and output are intentionally absent. */
|
|
560
|
-
onLegToolEvent?: (index: number, event: ToolEvent) => void,
|
|
561
|
-
): Promise<DelegateOutcome> {
|
|
562
|
-
if (params.tasks && params.tasks.length > 0) {
|
|
563
|
-
// Enforce the hard ceilings: cap the fan-out and clamp the concurrency the
|
|
564
|
-
// LLM requested — a strategy/tool call cannot exceed the declared limits.
|
|
565
|
-
const tasks = params.tasks.slice(0, limits.maxChildren);
|
|
566
|
-
const dropped = params.tasks.length - tasks.length;
|
|
567
|
-
const concurrency = normalizeDelegateConcurrency(params.concurrency, limits.maxConcurrency);
|
|
568
|
-
if (concurrency > 1) {
|
|
569
|
-
const writeSetError = validateParallelWriteSets(tasks);
|
|
570
|
-
if (writeSetError) return { text: writeSetError, results: [], views: [], ok: false };
|
|
571
|
-
}
|
|
572
|
-
const labels = tasks.map((t, i) => labelFor(t, i));
|
|
573
|
-
const views: DelegateView[] = tasks.map((t, i) => ({
|
|
574
|
-
agent: t.agent,
|
|
575
|
-
label: labels[i] as string,
|
|
576
|
-
running: true,
|
|
577
|
-
ok: false,
|
|
578
|
-
output: "",
|
|
579
|
-
activity: "",
|
|
580
|
-
usage: emptyUsage(),
|
|
581
|
-
}));
|
|
582
|
-
onProgress?.(views.map((v) => ({ ...v })));
|
|
583
|
-
|
|
584
|
-
const results = await mapWithConcurrency(tasks, concurrency, async (t, i) => {
|
|
585
|
-
const ac = new AbortController();
|
|
586
|
-
onLegStart?.(i, () => ac.abort());
|
|
587
|
-
// An engine that REJECTS becomes this leg's own ok:false result: the fan-out's completed
|
|
588
|
-
// siblings (already spawned and billed) must still reach the supervisor. Nothing in here
|
|
589
|
-
// is run-fatal — the max-children ceiling was applied to `tasks` before anything spawned.
|
|
590
|
-
let r: AgentResult;
|
|
591
|
-
try {
|
|
592
|
-
r = await engine.run(
|
|
593
|
-
specOf(t, i),
|
|
594
|
-
(p) => {
|
|
595
|
-
// Stream the leg's rolling output + current tool activity.
|
|
596
|
-
if (p.toolEvent) onLegToolEvent?.(i, p.toolEvent);
|
|
597
|
-
const cur = views[i] as DelegateView;
|
|
598
|
-
views[i] = { ...cur, output: p.output || cur.output, activity: p.activity ?? "" };
|
|
599
|
-
onProgress?.(views.map((v) => ({ ...v })));
|
|
600
|
-
},
|
|
601
|
-
ac.signal,
|
|
602
|
-
(steer) => onLegSteerable?.(i, steer),
|
|
603
|
-
);
|
|
604
|
-
} catch (err) {
|
|
605
|
-
r = legFailure(t.agent, err, ac.signal.aborted || (signal?.aborted ?? false));
|
|
606
|
-
}
|
|
607
|
-
views[i] = viewOf(t.agent, labels[i] as string, r);
|
|
608
|
-
onProgress?.(views.map((v) => ({ ...v })));
|
|
609
|
-
return r;
|
|
610
|
-
});
|
|
611
|
-
const agg = aggregateResults(results);
|
|
612
|
-
const text =
|
|
613
|
-
dropped > 0
|
|
614
|
-
? `${agg.output}\n\n[pi-persona] ${dropped} task(s) beyond the max-children limit (${limits.maxChildren}) were dropped.`
|
|
615
|
-
: agg.output;
|
|
616
|
-
return { text, results, views, ok: agg.ok };
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
if (params.agent && params.task) {
|
|
620
|
-
const single: DelegateTask = { agent: params.agent, task: params.task };
|
|
621
|
-
if (params.name) single.name = params.name;
|
|
622
|
-
if (params.skills && params.skills.length > 0) single.skills = params.skills;
|
|
623
|
-
if (params.model) single.model = params.model;
|
|
624
|
-
if (params.tools !== undefined) single.tools = params.tools;
|
|
625
|
-
if (params.role?.trim()) single.role = params.role.trim();
|
|
626
|
-
if (params.isolation !== undefined) single.isolation = params.isolation;
|
|
627
|
-
if (params.mcp !== undefined) single.mcp = params.mcp;
|
|
628
|
-
if (params.timeoutMs !== undefined) single.timeoutMs = params.timeoutMs;
|
|
629
|
-
if (params.brief !== undefined) single.brief = params.brief;
|
|
630
|
-
if (params.writeSet !== undefined) single.writeSet = params.writeSet;
|
|
631
|
-
if (params.outputContract !== undefined) single.outputContract = params.outputContract;
|
|
632
|
-
const label = labelFor(single, 0);
|
|
633
|
-
const view: DelegateView = {
|
|
634
|
-
agent: params.agent,
|
|
635
|
-
label,
|
|
636
|
-
running: true,
|
|
637
|
-
ok: false,
|
|
638
|
-
output: "",
|
|
639
|
-
activity: "",
|
|
640
|
-
usage: emptyUsage(),
|
|
641
|
-
};
|
|
642
|
-
onProgress?.([{ ...view }]);
|
|
643
|
-
const ac = new AbortController();
|
|
644
|
-
onLegStart?.(0, () => ac.abort());
|
|
645
|
-
const r = await engine.run(
|
|
646
|
-
specOf(single),
|
|
647
|
-
(p) => {
|
|
648
|
-
if (p.toolEvent) onLegToolEvent?.(0, p.toolEvent);
|
|
649
|
-
if (p.output) view.output = p.output;
|
|
650
|
-
view.activity = p.activity ?? "";
|
|
651
|
-
onProgress?.([{ ...view }]);
|
|
652
|
-
},
|
|
653
|
-
ac.signal,
|
|
654
|
-
(steer) => onLegSteerable?.(0, steer),
|
|
655
|
-
);
|
|
656
|
-
const text = r.ok ? r.output || "(no output)" : `Agent "${params.agent}" failed: ${r.error ?? "(no detail)"}`;
|
|
657
|
-
const terminalView = viewOf(params.agent, label, r);
|
|
658
|
-
onProgress?.([{ ...terminalView }]);
|
|
659
|
-
return { text, results: [r], views: [terminalView], ok: r.ok };
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
return { text: "delegate: provide either { agent, task } or { tasks: [{ agent, task }, ...] }", results: [], views: [], ok: false };
|
|
663
|
-
}
|
|
@@ -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>;
|