@aeondave/pi-persona 1.17.0 → 1.17.3
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 +79 -29
- package/CHANGELOG.md +264 -0
- package/README.md +27 -12
- 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 +223 -57
- package/docs/EXPERIENCE.md +19 -9
- package/docs/MONITORS.md +62 -3
- package/docs/REFERENCE.md +189 -35
- 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 +9 -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 +20 -0
- package/src/bridge.ts +3 -2
- package/src/core/brief.ts +429 -356
- package/src/core/config.ts +16 -11
- 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/seed.ts +99 -6
- package/src/core/session-runs.ts +87 -0
- package/src/core/settings.ts +251 -0
- package/src/core/timer.ts +34 -9
- package/src/engine/adapter.ts +12 -8
- package/src/engine/async.ts +195 -136
- package/src/engine/child.ts +1 -1
- package/src/engine/host-module.ts +12 -0
- package/src/engine/inproc.ts +11 -6
- package/src/engine/stream.ts +3 -0
- package/src/exocom/install.ts +2 -1
- package/src/extension/companion-settings.ts +29 -0
- package/src/extension/engine.ts +43 -16
- package/src/extension/hooks.ts +106 -36
- package/src/extension/persona-model-settings.ts +336 -0
- package/src/extension/persona-settings.ts +459 -0
- package/src/extension/shared.ts +15 -7
- package/src/extension.ts +642 -267
- package/src/monitor/archive.ts +235 -0
- package/src/monitor/session.ts +21 -1
- package/src/orchestration/flow-journal.ts +26 -5
- package/src/orchestration/flow-run.ts +38 -13
- package/src/orchestration/flow.ts +9 -2
- package/src/orchestration/reducers.ts +9 -5
- package/src/orchestration/render.ts +1 -1
- package/src/orchestration/sdk.ts +17 -3
- 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/persona/config-store.ts +6 -195
- package/src/persona/model-participants.ts +1 -1
- package/src/persona/persona.ts +7 -3
- package/src/persona/settings-store.ts +636 -0
- package/src/telemetry/contract.ts +1 -1
- package/src/tools/background-run.ts +151 -0
- package/src/tools/council.ts +77 -52
- package/src/tools/delegate-tool.ts +315 -285
- package/src/tools/delegate.ts +22 -210
- package/src/tools/exocom-work.ts +18 -53
- package/src/tools/exocom.ts +15 -19
- package/src/tools/flow.ts +85 -28
- package/src/tools/intercom-tool.ts +108 -118
- package/src/tools/intercom.ts +1 -1
- package/src/tools/models.ts +19 -9
- package/src/tools/monitor.ts +39 -6
- package/src/tools/timer.ts +11 -13
- package/src/ui/agent-overlay.ts +55 -14
- package/src/ui/agent-tree.ts +84 -18
- 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/persona-settings.ts +328 -0
- package/src/ui/presentation.ts +24 -0
- package/src/ui/report-body.ts +63 -0
- package/src/ui/session-cards.ts +189 -0
- package/src/ui/wake-rows.ts +262 -0
- package/teams.yaml +5 -1
- package/src/persona/state.ts +0 -28
package/src/tools/delegate.ts
CHANGED
|
@@ -1,28 +1,13 @@
|
|
|
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";
|
|
13
9
|
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";
|
|
10
|
+
import { type AgentRunSpec, type InfrastructureFailure, isInfrastructureFailure, isPositiveFiniteMs } from "../orchestration/sdk.ts";
|
|
26
11
|
|
|
27
12
|
export interface DelegateTask {
|
|
28
13
|
agent: string;
|
|
@@ -73,8 +58,6 @@ export interface DelegateParams {
|
|
|
73
58
|
outputContract?: string;
|
|
74
59
|
tasks?: DelegateTask[];
|
|
75
60
|
concurrency?: number;
|
|
76
|
-
async?: boolean;
|
|
77
|
-
sync?: boolean;
|
|
78
61
|
}
|
|
79
62
|
|
|
80
63
|
/** Structured cold-start context that any persona policy may require. */
|
|
@@ -299,21 +282,6 @@ export function coerceDelegateParams(raw: unknown): { ok: true; params: Delegate
|
|
|
299
282
|
return { ok: true, params: bag as DelegateParams };
|
|
300
283
|
}
|
|
301
284
|
|
|
302
|
-
/** A live per-sub-agent view for the UI (running → done/failed). */
|
|
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
285
|
/** Short model name for display: drop provider + `claude-` + any `:thinking`. */
|
|
318
286
|
export function shortModel(ref: string | undefined): string {
|
|
319
287
|
if (!ref) return "";
|
|
@@ -323,8 +291,7 @@ export function shortModel(ref: string | undefined): string {
|
|
|
323
291
|
|
|
324
292
|
/** The bare display NAME for a sub-agent (no model suffix): the loader's name, or a codename
|
|
325
293
|
* 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. */
|
|
294
|
+
* model itself (e.g. the async tree node, `AsyncRun.label`). */
|
|
328
295
|
export function nameFor(t: { agent: string; name?: string }, index: number): string {
|
|
329
296
|
// No catalog of call-signs: an unnamed generic operator stays the agent name, disambiguated
|
|
330
297
|
// by index. The model is supposed to invent `<call-sign>-<purpose>` itself; this is only the
|
|
@@ -333,28 +300,8 @@ export function nameFor(t: { agent: string; name?: string }, index: number): str
|
|
|
333
300
|
return sanitizeDisplayLabel(t.name?.trim() || fallback, fallback);
|
|
334
301
|
}
|
|
335
302
|
|
|
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. */
|
|
303
|
+
/** Normalize an LLM-authored concurrency request once for scheduling and policy gates.
|
|
304
|
+
* Fractional/non-finite inputs fail toward serialization instead of rounding up. */
|
|
358
305
|
export function normalizeDelegateConcurrency(value: number | undefined, maximum: number): number {
|
|
359
306
|
const ceiling = Math.max(1, Number.isFinite(maximum) ? Math.floor(maximum) : 1);
|
|
360
307
|
if (value === undefined) return ceiling;
|
|
@@ -363,9 +310,8 @@ export function normalizeDelegateConcurrency(value: number | undefined, maximum:
|
|
|
363
310
|
}
|
|
364
311
|
|
|
365
312
|
/** 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). */
|
|
313
|
+
* delegate path's field mapping (a second copy is how a per-leg knob like `timeoutMs` silently
|
|
314
|
+
* misses one launch route). */
|
|
369
315
|
export function specOf(t: DelegateTask, index = 0): AgentRunSpec {
|
|
370
316
|
const renderedBrief = renderDelegationBrief(t.brief);
|
|
371
317
|
const spec: AgentRunSpec = {
|
|
@@ -406,6 +352,18 @@ export interface DelegationKeyInput {
|
|
|
406
352
|
isolation?: string;
|
|
407
353
|
}
|
|
408
354
|
|
|
355
|
+
/** The ledger's view of a task or run spec: just the key fields, absent when unset. */
|
|
356
|
+
export function keyInputOf(t: Pick<DelegateTask, "agent" | "task" | "model" | "role" | "tools" | "isolation">): DelegationKeyInput {
|
|
357
|
+
return {
|
|
358
|
+
agent: t.agent,
|
|
359
|
+
task: t.task,
|
|
360
|
+
...(t.model ? { model: t.model } : {}),
|
|
361
|
+
...(t.role ? { role: t.role } : {}),
|
|
362
|
+
...(t.tools !== undefined ? { tools: t.tools } : {}),
|
|
363
|
+
...(t.isolation ? { isolation: t.isolation } : {}),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
409
367
|
export class DelegationLedger {
|
|
410
368
|
private readonly failures = new Map<string, number>();
|
|
411
369
|
private readonly maxKeys = 200;
|
|
@@ -469,9 +427,6 @@ export class DelegationLedger {
|
|
|
469
427
|
* an operator abort is cancellation, and an engine REJECTION is the harness breaking (see
|
|
470
428
|
* {@link isInfrastructureFailure}) — a full disk or a crashed adapter says nothing about whether the
|
|
471
429
|
* 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
430
|
*/
|
|
476
431
|
export function shouldRecordDelegationOutcome(result: Partial<InfrastructureFailure>): boolean {
|
|
477
432
|
return result.failureKind !== "abort" && !isInfrastructureFailure(result);
|
|
@@ -518,146 +473,3 @@ export function unknownContractError(requested: Array<string | undefined>, insta
|
|
|
518
473
|
"`outputContract` takes a contract NAME; omit it for free-form output and describe the report shape in `requiredArtifacts`."
|
|
519
474
|
);
|
|
520
475
|
}
|
|
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
|
-
}
|
package/src/tools/exocom-work.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { type Static, Type } from "typebox";
|
|
7
7
|
|
|
8
|
-
import { type ExtensionAPI
|
|
8
|
+
import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { Text } from "@earendil-works/pi-tui";
|
|
10
10
|
|
|
11
11
|
import { attributePeer } from "../core/fence.ts";
|
|
@@ -15,6 +15,8 @@ import type { ExocomPlane } from "../exocom/plane.ts";
|
|
|
15
15
|
import type { ExocomSemanticFrame } from "../exocom/envelope.ts";
|
|
16
16
|
import { EXOCOM_WAIT_MAX_MS, waitTimeoutMs } from "../exocom/wait.ts";
|
|
17
17
|
import { normalizeMetadataText } from "../exocom/registry.ts";
|
|
18
|
+
import { renderLiteralToolResult } from "../ui/literal-tool-result.ts";
|
|
19
|
+
import { createReportBody } from "../ui/report-body.ts";
|
|
18
20
|
|
|
19
21
|
const TOKEN = { minLength: 1, maxLength: 128, pattern: "^[A-Za-z0-9._:-]+$" } as const;
|
|
20
22
|
|
|
@@ -90,11 +92,6 @@ function result(text: string, details: Record<string, unknown>) {
|
|
|
90
92
|
return { content: [{ type: "text" as const, text }], details };
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
function workToolFailed(renderedResult: unknown): boolean {
|
|
94
|
-
return !!renderedResult && typeof renderedResult === "object"
|
|
95
|
-
&& (renderedResult as { isError?: unknown }).isError === true;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
95
|
export function registerExocomWorkTools(pi: ExtensionAPI, deps: ExocomWorkDeps): void {
|
|
99
96
|
const identity = () => {
|
|
100
97
|
const plane = deps.getPlane();
|
|
@@ -122,12 +119,7 @@ export function registerExocomWorkTools(pi: ExtensionAPI, deps: ExocomWorkDeps):
|
|
|
122
119
|
renderCall(args, theme) {
|
|
123
120
|
return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Claim "))}${theme.fg("accent", normalizeMetadataText(args.work_key, 80, "work"))}`, 0, 0);
|
|
124
121
|
},
|
|
125
|
-
renderResult
|
|
126
|
-
const first = res.content.find((item) => item.type === "text");
|
|
127
|
-
const text = first?.type === "text" ? first.text : "Exocom claim failed";
|
|
128
|
-
if (workToolFailed(res)) return new Text(theme.fg("error", text), 0, 0);
|
|
129
|
-
return new Text(theme.fg(expanded ? "toolOutput" : "accent", expanded ? text : `${text} (${keyHint("app.tools.expand", "to expand")})`), 0, 0);
|
|
130
|
-
},
|
|
122
|
+
renderResult: renderLiteralToolResult,
|
|
131
123
|
});
|
|
132
124
|
|
|
133
125
|
pi.registerTool({
|
|
@@ -161,12 +153,7 @@ export function registerExocomWorkTools(pi: ExtensionAPI, deps: ExocomWorkDeps):
|
|
|
161
153
|
renderCall(args, theme) {
|
|
162
154
|
return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Ask "))}${theme.fg("accent", normalizeMetadataText(args.target, 80, "peer"))}`, 0, 0);
|
|
163
155
|
},
|
|
164
|
-
renderResult
|
|
165
|
-
const first = res.content.find((item) => item.type === "text");
|
|
166
|
-
const text = first?.type === "text" ? first.text : "Exocom ask failed";
|
|
167
|
-
if (workToolFailed(res)) return new Text(theme.fg("error", text), 0, 0);
|
|
168
|
-
return new Text(theme.fg(expanded ? "toolOutput" : "accent", expanded ? text : `${text} (${keyHint("app.tools.expand", "to expand")})`), 0, 0);
|
|
169
|
-
},
|
|
156
|
+
renderResult: renderLiteralToolResult,
|
|
170
157
|
});
|
|
171
158
|
|
|
172
159
|
pi.registerTool({
|
|
@@ -191,12 +178,7 @@ export function registerExocomWorkTools(pi: ExtensionAPI, deps: ExocomWorkDeps):
|
|
|
191
178
|
renderCall(args, theme) {
|
|
192
179
|
return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Answer "))}${theme.fg("accent", normalizeMetadataText(args.ask_id, 80, "ask"))}`, 0, 0);
|
|
193
180
|
},
|
|
194
|
-
renderResult
|
|
195
|
-
const first = res.content.find((item) => item.type === "text");
|
|
196
|
-
const text = first?.type === "text" ? first.text : "Exocom answer failed";
|
|
197
|
-
if (workToolFailed(res)) return new Text(theme.fg("error", text), 0, 0);
|
|
198
|
-
return new Text(theme.fg(expanded ? "toolOutput" : "accent", expanded ? text : `${text} (${keyHint("app.tools.expand", "to expand")})`), 0, 0);
|
|
199
|
-
},
|
|
181
|
+
renderResult: renderLiteralToolResult,
|
|
200
182
|
});
|
|
201
183
|
|
|
202
184
|
pi.registerTool({
|
|
@@ -221,12 +203,7 @@ export function registerExocomWorkTools(pi: ExtensionAPI, deps: ExocomWorkDeps):
|
|
|
221
203
|
renderCall(args, theme) {
|
|
222
204
|
return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Decline "))}${theme.fg("accent", normalizeMetadataText(args.ask_id, 80, "ask"))}`, 0, 0);
|
|
223
205
|
},
|
|
224
|
-
renderResult
|
|
225
|
-
const first = res.content.find((item) => item.type === "text");
|
|
226
|
-
const text = first?.type === "text" ? first.text : "Exocom decline failed";
|
|
227
|
-
if (workToolFailed(res)) return new Text(theme.fg("error", text), 0, 0);
|
|
228
|
-
return new Text(theme.fg(expanded ? "toolOutput" : "accent", expanded ? text : `${text} (${keyHint("app.tools.expand", "to expand")})`), 0, 0);
|
|
229
|
-
},
|
|
206
|
+
renderResult: renderLiteralToolResult,
|
|
230
207
|
});
|
|
231
208
|
|
|
232
209
|
pi.registerTool({
|
|
@@ -250,11 +227,14 @@ export function registerExocomWorkTools(pi: ExtensionAPI, deps: ExocomWorkDeps):
|
|
|
250
227
|
renderCall(args, theme) {
|
|
251
228
|
return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Wait "))}${theme.fg("accent", normalizeMetadataText(args.work_key, 80, "work"))}`, 0, 0);
|
|
252
229
|
},
|
|
253
|
-
renderResult(res,
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (
|
|
257
|
-
|
|
230
|
+
renderResult(res, options, theme, context) {
|
|
231
|
+
// A retained answer is peer prose, not the literal receipt for arming a wait.
|
|
232
|
+
// Its ok=false can be a valid negative answer; only the host error flag is failure.
|
|
233
|
+
if (options.expanded && !(context?.isError ?? res.isError) && res.details?.status === "answered") {
|
|
234
|
+
const text = res.content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
|
|
235
|
+
return createReportBody(text, theme, {}, context?.lastComponent);
|
|
236
|
+
}
|
|
237
|
+
return renderLiteralToolResult(res, options, theme, context);
|
|
258
238
|
},
|
|
259
239
|
});
|
|
260
240
|
|
|
@@ -274,12 +254,7 @@ export function registerExocomWorkTools(pi: ExtensionAPI, deps: ExocomWorkDeps):
|
|
|
274
254
|
renderCall(args, theme) {
|
|
275
255
|
return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Release "))}${theme.fg("accent", normalizeMetadataText(args.work_key, 80, "work"))}`, 0, 0);
|
|
276
256
|
},
|
|
277
|
-
renderResult
|
|
278
|
-
const first = res.content.find((item) => item.type === "text");
|
|
279
|
-
const text = first?.type === "text" ? first.text : "Exocom release failed";
|
|
280
|
-
if (workToolFailed(res)) return new Text(theme.fg("error", text), 0, 0);
|
|
281
|
-
return new Text(theme.fg(expanded ? "toolOutput" : "accent", expanded ? text : `${text} (${keyHint("app.tools.expand", "to expand")})`), 0, 0);
|
|
282
|
-
},
|
|
257
|
+
renderResult: renderLiteralToolResult,
|
|
283
258
|
});
|
|
284
259
|
|
|
285
260
|
pi.registerTool({
|
|
@@ -298,12 +273,7 @@ export function registerExocomWorkTools(pi: ExtensionAPI, deps: ExocomWorkDeps):
|
|
|
298
273
|
renderCall(args, theme) {
|
|
299
274
|
return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Progress "))}${theme.fg("accent", normalizeMetadataText(args.work_key, 80, "work"))}`, 0, 0);
|
|
300
275
|
},
|
|
301
|
-
renderResult
|
|
302
|
-
const first = res.content.find((item) => item.type === "text");
|
|
303
|
-
const text = first?.type === "text" ? first.text : "Exocom progress failed";
|
|
304
|
-
if (workToolFailed(res)) return new Text(theme.fg("error", text), 0, 0);
|
|
305
|
-
return new Text(theme.fg(expanded ? "toolOutput" : "accent", expanded ? text : `${text} (${keyHint("app.tools.expand", "to expand")})`), 0, 0);
|
|
306
|
-
},
|
|
276
|
+
renderResult: renderLiteralToolResult,
|
|
307
277
|
});
|
|
308
278
|
|
|
309
279
|
pi.registerTool({
|
|
@@ -318,11 +288,6 @@ export function registerExocomWorkTools(pi: ExtensionAPI, deps: ExocomWorkDeps):
|
|
|
318
288
|
renderCall(_args, theme) {
|
|
319
289
|
return new Text(theme.fg("toolTitle", theme.bold("Exocom Status")), 0, 0);
|
|
320
290
|
},
|
|
321
|
-
renderResult
|
|
322
|
-
const first = res.content.find((item) => item.type === "text");
|
|
323
|
-
const text = first?.type === "text" ? first.text : "Exocom status failed";
|
|
324
|
-
if (workToolFailed(res)) return new Text(theme.fg("error", text), 0, 0);
|
|
325
|
-
return new Text(theme.fg(expanded ? "toolOutput" : "accent", expanded ? text : `${text} (${keyHint("app.tools.expand", "to expand")})`), 0, 0);
|
|
326
|
-
},
|
|
291
|
+
renderResult: renderLiteralToolResult,
|
|
327
292
|
});
|
|
328
293
|
}
|
package/src/tools/exocom.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { type Static, Type } from "typebox";
|
|
|
19
19
|
|
|
20
20
|
import { keyHint, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
21
21
|
import { Text } from "@earendil-works/pi-tui";
|
|
22
|
+
import { renderLiteralToolResult } from "../ui/literal-tool-result.ts";
|
|
22
23
|
|
|
23
24
|
import { fencePeer } from "../core/fence.ts";
|
|
24
25
|
import { inventedExocomNameHint } from "../core/naming.ts";
|
|
@@ -227,12 +228,11 @@ function isSendDetails(details: unknown): details is ExocomSendDetails {
|
|
|
227
228
|
return Array.isArray(shape.failed) || typeof shape.msg_id === "string";
|
|
228
229
|
}
|
|
229
230
|
|
|
230
|
-
function renderSendResult(details: ExocomSendDetails
|
|
231
|
+
function renderSendResult(details: ExocomSendDetails): string {
|
|
231
232
|
if ("failed" in details) {
|
|
232
233
|
const failed = details.failedCount ?? details.failed.length;
|
|
233
234
|
const queued = details.queuedCount ?? details.msg_ids.length;
|
|
234
|
-
const summary =
|
|
235
|
-
if (!expanded) return `${summary} (${keyHint("app.tools.expand", "to expand")})`;
|
|
235
|
+
const summary = `queued ${queued} peer${queued === 1 ? "" : "s"}${failed > 0 ? ` · ${failed} failed` : ""}`;
|
|
236
236
|
const lines = [summary];
|
|
237
237
|
if (details.msg_ids.length > 0) lines.push(`msg_ids: ${details.msg_ids.join(", ")}`);
|
|
238
238
|
for (const failure of details.failed) {
|
|
@@ -242,8 +242,7 @@ function renderSendResult(details: ExocomSendDetails, expanded: boolean): string
|
|
|
242
242
|
if ((details.omittedFailures ?? 0) > 0) lines.push(`…and ${details.omittedFailures} more failures omitted; retry only the failed targets shown above.`);
|
|
243
243
|
return lines.join("\n");
|
|
244
244
|
}
|
|
245
|
-
|
|
246
|
-
return expanded ? summary : `${summary} (${keyHint("app.tools.expand", "to expand")})`;
|
|
245
|
+
return `queued to ${details.target} · msg_id=${details.msg_id}`;
|
|
247
246
|
}
|
|
248
247
|
|
|
249
248
|
/** `getPlane` is a LIVE accessor, not a captured value: a `canUseBus` downgrade (`stopExocom`,
|
|
@@ -294,7 +293,7 @@ export function registerExocomTools(
|
|
|
294
293
|
renderCall(_args, theme) {
|
|
295
294
|
return new Text(theme.fg("toolTitle", theme.bold("Exocom List")), 0, 0);
|
|
296
295
|
},
|
|
297
|
-
renderResult(result, { expanded }, theme) {
|
|
296
|
+
renderResult(result, { expanded }, theme, context) {
|
|
298
297
|
const details = result.details as unknown as {
|
|
299
298
|
peers?: ExocomPeerSummary[];
|
|
300
299
|
total?: number;
|
|
@@ -303,9 +302,8 @@ export function registerExocomTools(
|
|
|
303
302
|
scopeCode?: string;
|
|
304
303
|
homeWorkspace?: { label?: string; code?: string; sameAsScope?: boolean };
|
|
305
304
|
} | undefined;
|
|
306
|
-
if (!details || !Array.isArray(details.peers)) {
|
|
307
|
-
|
|
308
|
-
return new Text(theme.fg("error", first?.type === "text" ? first.text : "Exocom list failed"), 0, 0);
|
|
305
|
+
if (context?.isError || !details || !Array.isArray(details.peers)) {
|
|
306
|
+
return renderLiteralToolResult(result, { expanded }, theme, { isError: true });
|
|
309
307
|
}
|
|
310
308
|
const peers = details.peers;
|
|
311
309
|
const total = typeof details.total === "number" ? details.total : peers.length;
|
|
@@ -448,15 +446,14 @@ export function registerExocomTools(
|
|
|
448
446
|
const suffix = preview.length < message.length ? "…" : "";
|
|
449
447
|
return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Send "))}${theme.fg("accent", target)}${preview ? theme.fg("dim", ` ${preview}${suffix}`) : ""}`, 0, 0);
|
|
450
448
|
},
|
|
451
|
-
renderResult(result, { expanded }, theme) {
|
|
449
|
+
renderResult(result, { expanded }, theme, context) {
|
|
452
450
|
const details = result.details as unknown as ExocomSendDetails | undefined;
|
|
453
451
|
// pi renders this for a FAILED call too, handing us the error's empty details object — an
|
|
454
452
|
// unrecognised shape is a failure to show, not a send to congratulate the user on.
|
|
455
|
-
if (!isSendDetails(details)) {
|
|
456
|
-
|
|
457
|
-
return new Text(theme.fg("error", text?.type === "text" ? text.text : "(no result)"), 0, 0);
|
|
453
|
+
if (context?.isError || !isSendDetails(details)) {
|
|
454
|
+
return renderLiteralToolResult(result, { expanded }, theme, { isError: true });
|
|
458
455
|
}
|
|
459
|
-
return
|
|
456
|
+
return renderLiteralToolResult({ ...result, content: [{ type: "text", text: renderSendResult(details) }] }, { expanded }, theme, context);
|
|
460
457
|
},
|
|
461
458
|
});
|
|
462
459
|
|
|
@@ -475,15 +472,14 @@ export function registerExocomTools(
|
|
|
475
472
|
renderCall(args, theme) {
|
|
476
473
|
return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Name "))}${theme.fg("accent", normalizeMetadataText(args.name, 96, "peer"))}`, 0, 0);
|
|
477
474
|
},
|
|
478
|
-
renderResult(result, { expanded }, theme) {
|
|
475
|
+
renderResult(result, { expanded }, theme, context) {
|
|
479
476
|
const details = result.details as unknown as { name?: string } | undefined;
|
|
480
|
-
if (!details?.name) {
|
|
481
|
-
|
|
482
|
-
return new Text(theme.fg("error", first?.type === "text" ? first.text : "Exocom name failed"), 0, 0);
|
|
477
|
+
if (context?.isError || !details?.name) {
|
|
478
|
+
return renderLiteralToolResult(result, { expanded }, theme, { isError: true });
|
|
483
479
|
}
|
|
484
480
|
const name = normalizeMetadataText(details.name, 96, "(unchanged)");
|
|
485
481
|
const text = `exocom: you are now "${name}"`;
|
|
486
|
-
return
|
|
482
|
+
return renderLiteralToolResult({ ...result, content: [{ type: "text", text }] }, { expanded }, theme, context);
|
|
487
483
|
},
|
|
488
484
|
});
|
|
489
485
|
}
|