@jameslovespancakes/pi-plus 1.0.20 → 1.0.21
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/README.md +21 -3
- package/package.json +1 -1
- package/src/domains/workflows/index.ts +56 -104
- package/src/domains/workflows/runtime/advisory-challenge.ts +3 -3
- package/src/domains/workflows/runtime/agent-attempt.ts +3 -3
- package/src/domains/workflows/runtime/agent-options.ts +18 -0
- package/src/domains/workflows/runtime/agent-runner-types.ts +7 -3
- package/src/domains/workflows/runtime/agent-runner.ts +14 -6
- package/src/domains/workflows/runtime/agent-session.ts +34 -3
- package/src/domains/workflows/runtime/cancellation.ts +5 -0
- package/src/domains/workflows/runtime/engine.ts +19 -40
- package/src/domains/workflows/runtime/journal.ts +4 -4
- package/src/domains/workflows/runtime/live-agent.ts +37 -0
- package/src/domains/workflows/runtime/model-profiles.ts +2 -6
- package/src/domains/workflows/runtime/progress-types.ts +3 -1
- package/src/domains/workflows/runtime/progress.ts +54 -14
- package/src/domains/workflows/runtime/review/review-fix-workflow.ts +3 -3
- package/src/domains/workflows/runtime/types.ts +16 -15
- package/src/domains/workflows/runtime/ui/agent-transcript.ts +59 -0
- package/src/domains/workflows/runtime/ui/workflow-format.ts +6 -2
- package/src/domains/workflows/runtime/ui/workflow-inspector.ts +75 -61
- package/src/domains/workflows/runtime/ui/workflow-widget.ts +26 -66
- package/src/domains/workflows/runtime/workflow-advisory-utils.ts +5 -5
- package/src/domains/workflows/runtime/{background-workflows.ts → workflow-lifecycle.ts} +91 -75
- package/src/domains/workflows/runtime/workflow-management.ts +66 -0
- package/src/domains/workflows/runtime/workflow-run-controller.ts +16 -20
- package/src/domains/workflows/runtime/{workflow-run-background.ts → workflow-run-delivery.ts} +3 -3
- package/src/domains/workflows/runtime/workflow-run-record.ts +7 -4
- package/src/domains/workflows/workflows/code-review.ts +1 -1
- package/src/domains/workflows/workflows/diagnose.ts +1 -1
- package/src/domains/workflows/workflows/perf-review.ts +1 -1
- package/src/domains/workflows/workflows/refactor-scout.ts +1 -1
- package/src/domains/workflows/workflows/research.ts +4 -4
- package/src/domains/workflows/runtime/background-workflow-tool.ts +0 -75
package/README.md
CHANGED
|
@@ -124,7 +124,8 @@ boundary, including workflow subagents—not just through prompt instructions.
|
|
|
124
124
|
|
|
125
125
|
**Turn repeatable tasks into coordinated agent runs.** Built-in reviews,
|
|
126
126
|
diagnostics, research, and refactoring workflows support parallel agents,
|
|
127
|
-
worktree isolation, replay,
|
|
127
|
+
worktree isolation, replay, and usage accounting. Every run returns immediately;
|
|
128
|
+
results arrive when it finishes. There is no separate foreground/background mode.
|
|
128
129
|
|
|
129
130
|
```text
|
|
130
131
|
Task → parallel agents → findings → result
|
|
@@ -135,8 +136,25 @@ worktree isolation, replay, background execution, and usage accounting.
|
|
|
135
136
|
/workflow research "Compare the available approaches"
|
|
136
137
|
```
|
|
137
138
|
|
|
138
|
-
**`/workflow`** opens the running agent board.
|
|
139
|
-
|
|
139
|
+
**`/workflow`** opens the running agent board. Enter inspects, Esc goes back,
|
|
140
|
+
and X stops the selected agent from the list. Inspection needs at least 80×24.
|
|
141
|
+
The inspector uses pi's native message/tool rendering and editor. Enter steers;
|
|
142
|
+
Alt+Enter queues a follow-up. `/model provider/model` and `/thinking level`
|
|
143
|
+
affect only that agent. Other parent-session commands are not forwarded.
|
|
144
|
+
|
|
145
|
+
Every `api.agent()` call requires `label`, `model`, and `thinkingLevel`.
|
|
146
|
+
Built-ins resolve explicit routes with `api.modelProfile("small" | "medium")`;
|
|
147
|
+
configure both routes in `.pi/workflow-models.json` (or the agent-directory
|
|
148
|
+
`workflow-models.json`), each with `model: "provider/model-id"` and
|
|
149
|
+
`thinkingLevel`. Missing routes now fail rather than inheriting the host model.
|
|
150
|
+
The file shape is `{ "profiles": { "small": { "model": "provider/model-id",
|
|
151
|
+
"thinkingLevel": "low" }, "medium": { "model": "provider/model-id",
|
|
152
|
+
"thinkingLevel": "high" } } }`.
|
|
153
|
+
|
|
154
|
+
The main agent can use `workflow({ action: "list" })`, or `inspect`/`stop` with
|
|
155
|
+
`runId` and optionally `agentId`. Activity is fetched on demand, not injected
|
|
156
|
+
into every turn. Limits on concurrency, agents, time, and output tokens remain
|
|
157
|
+
optional.
|
|
140
158
|
|
|
141
159
|
## Remote Workers
|
|
142
160
|
|
package/package.json
CHANGED
|
@@ -5,8 +5,7 @@ import { Text, type AutocompleteItem } from "@earendil-works/pi-tui";
|
|
|
5
5
|
import { isAdvisoryReport } from "./runtime/advisory-schema.ts";
|
|
6
6
|
import type { WorkflowProgressSnapshot } from "./runtime/progress-types.ts";
|
|
7
7
|
import type { LoadedWorkflow, WorkflowProgressSource, WorkflowRef, WorkflowRunMetadata, WorkflowRunOptions } from "./runtime/types.ts";
|
|
8
|
-
import { WorkflowInspector } from "./runtime/ui/workflow-inspector.ts";
|
|
9
|
-
import { WORKFLOW_VIEWER_OVERLAY_OPTIONS } from "./runtime/ui/workflow-viewer-layout.ts";
|
|
8
|
+
import { WorkflowInspector, WORKFLOW_INSPECTOR_OVERLAY_OPTIONS } from "./runtime/ui/workflow-inspector.ts";
|
|
10
9
|
import type { PerfSink } from "./runtime/perf.ts";
|
|
11
10
|
import type { WorkflowUsageSnapshot } from "./runtime/usage.ts";
|
|
12
11
|
import { ADAPTIVE_WORKFLOW_GUIDANCE, registerDynamax } from "./runtime/dynamax.ts";
|
|
@@ -37,8 +36,7 @@ import {
|
|
|
37
36
|
WORKFLOW_USAGE_LIMIT_DELAY_MIN_MS,
|
|
38
37
|
} from "./runtime/options.ts";
|
|
39
38
|
import { executeWorkflowInvocation, type WorkflowExecution, type WorkflowPerfDetails } from "./runtime/workflow-execution.ts";
|
|
40
|
-
import {
|
|
41
|
-
import { backgroundUnavailableResult, startBackgroundWorkflowTool } from "./runtime/background-workflow-tool.ts";
|
|
39
|
+
import { WorkflowLifecycle, workflowUnavailableResult } from "./runtime/workflow-lifecycle.ts";
|
|
42
40
|
import { WorkflowRunController } from "./runtime/workflow-run-controller.ts";
|
|
43
41
|
import { completeCurrentArgument, splitArgumentPrefix } from "./runtime/command-completions.ts";
|
|
44
42
|
import { assertSupportedPiVersion } from "./runtime/pi-compat.ts";
|
|
@@ -132,7 +130,6 @@ export async function resolveWorkflowRef(ref: WorkflowRef, perf?: PerfSink): Pro
|
|
|
132
130
|
}
|
|
133
131
|
|
|
134
132
|
const WORKFLOW_OPTION_COMPLETIONS = [
|
|
135
|
-
{ value: "--inspect", description: "Open the live workflow inspector" },
|
|
136
133
|
{ value: "--refresh", description: "Refresh dynamic workflow discovery" },
|
|
137
134
|
{ value: "--perf", description: "Collect workflow performance metrics" },
|
|
138
135
|
{ value: "--result-viewer", description: "Open supported result viewers" },
|
|
@@ -195,9 +192,10 @@ export async function openWorkflowInspector(ctx: ExtensionContext, inspection: A
|
|
|
195
192
|
() => done(undefined),
|
|
196
193
|
undefined,
|
|
197
194
|
source,
|
|
195
|
+
_keybindings,
|
|
198
196
|
);
|
|
199
197
|
},
|
|
200
|
-
|
|
198
|
+
WORKFLOW_INSPECTOR_OVERLAY_OPTIONS,
|
|
201
199
|
);
|
|
202
200
|
} finally {
|
|
203
201
|
unsubscribe?.();
|
|
@@ -263,7 +261,7 @@ function parseWorkflowOptions(input: string): { args: string; options: WorkflowR
|
|
|
263
261
|
for (let i = 0; i < tokens.length; i++) {
|
|
264
262
|
const token = tokens[i];
|
|
265
263
|
if (token === "--inspect") {
|
|
266
|
-
|
|
264
|
+
optionErrors.push("--inspect was removed; open the running workflow with /workflow");
|
|
267
265
|
continue;
|
|
268
266
|
}
|
|
269
267
|
if (token === "--refresh") {
|
|
@@ -413,7 +411,6 @@ export interface WorkflowToolRequestParams {
|
|
|
413
411
|
readonly name?: string;
|
|
414
412
|
readonly script?: string;
|
|
415
413
|
readonly resumeFromRunId?: string;
|
|
416
|
-
readonly background?: boolean;
|
|
417
414
|
}
|
|
418
415
|
|
|
419
416
|
export type WorkflowToolRequest =
|
|
@@ -445,44 +442,6 @@ export function inlineCompileErrorResult(message: string): WorkflowToolErrorResu
|
|
|
445
442
|
return { content: [{ type: "text", text: `Inline workflow did not compile: ${message}` }], details: { error: "inline_compile_error", message } };
|
|
446
443
|
}
|
|
447
444
|
|
|
448
|
-
export async function sendWorkflowResult(
|
|
449
|
-
pi: ExtensionAPI,
|
|
450
|
-
ctx: ExtensionContext,
|
|
451
|
-
name: string,
|
|
452
|
-
mod: LoadedWorkflow,
|
|
453
|
-
args: string,
|
|
454
|
-
options: WorkflowRunOptions,
|
|
455
|
-
perfRecorder?: PerfSink,
|
|
456
|
-
reviewSessions: ReviewSessionCoordinator = createReviewSessionCoordinator(pi),
|
|
457
|
-
): Promise<void> {
|
|
458
|
-
await sendResolvedWorkflowResult(
|
|
459
|
-
pi,
|
|
460
|
-
ctx,
|
|
461
|
-
name,
|
|
462
|
-
mod,
|
|
463
|
-
args,
|
|
464
|
-
resolveWorkflowRunOptions(options),
|
|
465
|
-
perfRecorder,
|
|
466
|
-
reviewSessions,
|
|
467
|
-
);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
async function sendResolvedWorkflowResult(
|
|
471
|
-
pi: ExtensionAPI,
|
|
472
|
-
ctx: ExtensionContext,
|
|
473
|
-
name: string,
|
|
474
|
-
mod: LoadedWorkflow,
|
|
475
|
-
args: string,
|
|
476
|
-
options: ResolvedWorkflowRunOptions,
|
|
477
|
-
perfRecorder: PerfSink | undefined,
|
|
478
|
-
reviewSessions: ReviewSessionCoordinator,
|
|
479
|
-
): Promise<void> {
|
|
480
|
-
const execution = await executeResolvedWorkflow(pi, ctx, name, mod, args, options, perfRecorder);
|
|
481
|
-
reviewSessions.remember(ctx, execution, options);
|
|
482
|
-
sendWorkflowExecution(pi, execution);
|
|
483
|
-
await reviewSessions.present(ctx, execution, options);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
445
|
async function executeResolvedWorkflow(
|
|
487
446
|
pi: ExtensionAPI,
|
|
488
447
|
ctx: ExtensionContext,
|
|
@@ -548,8 +507,8 @@ function createReviewSessionCoordinator(pi: ExtensionAPI): ReviewSessionCoordina
|
|
|
548
507
|
export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShortcuts = resolveDynamaxShortcuts()): void {
|
|
549
508
|
assertSupportedPiVersion(VERSION);
|
|
550
509
|
const reviewSessions = createReviewSessionCoordinator(pi);
|
|
551
|
-
const
|
|
552
|
-
const workflowRuns = new WorkflowRunController(
|
|
510
|
+
const lifecycle = new WorkflowLifecycle(pi);
|
|
511
|
+
const workflowRuns = new WorkflowRunController(lifecycle, {
|
|
553
512
|
async resolveWorkflow(name) {
|
|
554
513
|
const { discoverWorkflows } = await loadDiscovery();
|
|
555
514
|
return (await discoverWorkflows(EXTENSION_DIR)).get(name);
|
|
@@ -560,18 +519,18 @@ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShort
|
|
|
560
519
|
reviewSessions.remember(ctx, execution, options);
|
|
561
520
|
},
|
|
562
521
|
});
|
|
563
|
-
|
|
522
|
+
lifecycle.onRunSettled((ctx, runId) => workflowRuns.runSettled(ctx, runId));
|
|
564
523
|
registerDynamax(pi, shortcuts, { openInspector: (ctx) => openAvailableWorkflowInspector(pi, ctx) });
|
|
565
524
|
pi.on("session_start", async (_event, ctx) => {
|
|
566
|
-
await
|
|
525
|
+
await lifecycle.sessionStarted(ctx);
|
|
567
526
|
await workflowRuns.sessionStarted(ctx);
|
|
568
527
|
});
|
|
569
528
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
570
|
-
await
|
|
529
|
+
await lifecycle.agentSettled(ctx);
|
|
571
530
|
});
|
|
572
531
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
573
532
|
workflowRuns.sessionShutdown(ctx);
|
|
574
|
-
await
|
|
533
|
+
await lifecycle.sessionShutdown(ctx);
|
|
575
534
|
const key = sessionKey(ctx);
|
|
576
535
|
workflowInspections.get(pi)?.delete(key);
|
|
577
536
|
reviewSessions.dispose(ctx);
|
|
@@ -619,24 +578,36 @@ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShort
|
|
|
619
578
|
ctx.ui.notify(`Unknown workflow "${direct.name}". Available: ${available}`, "error");
|
|
620
579
|
return;
|
|
621
580
|
}
|
|
622
|
-
|
|
581
|
+
const unavailable = workflowUnavailableResult(ctx.mode);
|
|
582
|
+
if (unavailable) {
|
|
583
|
+
ctx.ui.notify(unavailable.content[0].text, "warning");
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
const started = await lifecycle.launch({
|
|
587
|
+
ctx, name: direct.name, options: directOptions,
|
|
588
|
+
async execute(runCtx, options) {
|
|
589
|
+
const execution = await executeResolvedWorkflow(pi, runCtx, direct.name, mod, direct.args, options, perfRecorder);
|
|
590
|
+
reviewSessions.remember(ctx, execution, options);
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
ctx.ui.notify(started.content[0].text, started.details.error ? "error" : "info");
|
|
623
594
|
},
|
|
624
595
|
});
|
|
625
596
|
|
|
626
|
-
registerWorkflowTool(pi, reviewSessions,
|
|
597
|
+
registerWorkflowTool(pi, reviewSessions, lifecycle);
|
|
627
598
|
}
|
|
628
599
|
|
|
629
600
|
/** Register the host-facing workflow tool independently from command and lifecycle surfaces. */
|
|
630
601
|
function registerWorkflowTool(
|
|
631
602
|
pi: ExtensionAPI,
|
|
632
603
|
reviewSessions: ReviewSessionCoordinator,
|
|
633
|
-
|
|
604
|
+
lifecycle: WorkflowLifecycle,
|
|
634
605
|
): void {
|
|
635
606
|
pi.registerTool({
|
|
636
607
|
name: "workflow",
|
|
637
608
|
label: "Workflow",
|
|
638
609
|
description:
|
|
639
|
-
"ONLY call workflow when the user opted in with the literal token `dynamax`, explicitly requested a workflow, or invoked a command or skill that requires one.
|
|
610
|
+
"ONLY call workflow when the user opted in with the literal token `dynamax`, explicitly requested a workflow, or invoked a command or skill that requires one. Starts named or inline multi-agent workflows and returns a run ID immediately. Use list, inspect, or stop to manage runs and individual agents.",
|
|
640
611
|
promptSnippet: "Run an existing named workflow or an inline one-off workflow script",
|
|
641
612
|
promptGuidelines: [
|
|
642
613
|
"Use workflow only after a `dynamax` opt-in, an explicit workflow request, or a command or skill instruction.",
|
|
@@ -650,12 +621,16 @@ function registerWorkflowTool(
|
|
|
650
621
|
"If an inline subagent needs grep/find/code-search helpers, use `tools: [\"read\", \"bash\", \"grep\", \"find\", \"ls\"]` plus `toolHints: [\"search\"]` so installed tools such as ast-grep, mgrep, ffgrep, or fffind are discovered dynamically.",
|
|
651
622
|
"`api.budget` exposes `{ total, spent(), remaining() }` (output tokens). When the run is budgeted, scale fleets from `budget.total` and guard loops with `while (budget.total && budget.remaining() > N) { await api.agent(...) }`; `api.agent()` throws once the ceiling is reached.",
|
|
652
623
|
ADAPTIVE_WORKFLOW_GUIDANCE,
|
|
653
|
-
"
|
|
654
|
-
"
|
|
624
|
+
"All runs return a durable run ID immediately; completion is delivered later. Use action list/inspect/stop to observe or cancel without launching another workflow.",
|
|
625
|
+
"Every api.agent() call must explicitly supply label, model, and thinkingLevel; no implicit host model or thinking defaults.",
|
|
626
|
+
"Set autoResumeOnUsageLimit: true only when the user wants bounded automatic continuation after a recognized provider usage window.",
|
|
655
627
|
"Set resumeEditedWorkflow: true only with resumeFromRunId when the user explicitly accepts reusing behaviorally identical calls after workflow source edits.",
|
|
656
|
-
"
|
|
628
|
+
"Launch calls must provide exactly one of name or script. Management calls use action, runId, and optionally agentId instead.",
|
|
657
629
|
],
|
|
658
630
|
parameters: Type.Object({
|
|
631
|
+
action: Type.Optional(Type.Union([Type.Literal("start"), Type.Literal("list"), Type.Literal("inspect"), Type.Literal("stop")], { description: "Defaults to start. Management actions do not launch a workflow." })),
|
|
632
|
+
runId: Type.Optional(Type.String({ minLength: 1, description: "Run to inspect or stop" })),
|
|
633
|
+
agentId: Type.Optional(Type.Integer({ minimum: 1, description: "Inspect or stop only this agent" })),
|
|
659
634
|
name: Type.Optional(Type.String({ description: "Workflow name, e.g. code-review. Provide exactly one of name or script." })),
|
|
660
635
|
script: Type.Optional(Type.String({ description: "Inline workflow script. Provide exactly one of script or name." })),
|
|
661
636
|
args: Type.Optional(Type.String({ description: "Arguments for the workflow (e.g. target or focus)" })),
|
|
@@ -677,7 +652,7 @@ function registerWorkflowTool(
|
|
|
677
652
|
}),
|
|
678
653
|
),
|
|
679
654
|
autoResumeOnUsageLimit: Type.Optional(
|
|
680
|
-
Type.Boolean({ description: "
|
|
655
|
+
Type.Boolean({ description: "Opt into bounded automatic resume after a recognized provider usage limit" }),
|
|
681
656
|
),
|
|
682
657
|
usageLimitMaxAttempts: Type.Optional(
|
|
683
658
|
Type.Integer({
|
|
@@ -705,17 +680,16 @@ function registerWorkflowTool(
|
|
|
705
680
|
resumeEditedWorkflow: Type.Optional(
|
|
706
681
|
Type.Boolean({ description: "With resumeFromRunId, ignore only workflow-source fingerprint changes while retaining all other replay checks" }),
|
|
707
682
|
),
|
|
708
|
-
background: Type.Optional(Type.Boolean({ description: "Return a durable run ID immediately and deliver completion to this conversation later" })),
|
|
709
683
|
}),
|
|
710
684
|
renderCall(args, theme) {
|
|
711
685
|
const suffix = args.args ? ` ${theme.fg("dim", args.args)}` : "";
|
|
712
|
-
|
|
686
|
+
if (args.action && args.action !== "start") return new Text(`▸ ${theme.fg("toolTitle", "workflow")} ${args.action} ${args.runId ?? ""}`, 0, 0);
|
|
713
687
|
if (args.name?.trim()) {
|
|
714
|
-
return new Text(`▸ ${theme.fg("toolTitle", theme.bold("workflow"))} ${theme.fg("accent", args.name.trim())}${
|
|
688
|
+
return new Text(`▸ ${theme.fg("toolTitle", theme.bold("workflow"))} ${theme.fg("accent", args.name.trim())}${suffix}`, 0, 0);
|
|
715
689
|
}
|
|
716
690
|
const preview = compactInlinePreview(args.script);
|
|
717
691
|
const previewSuffix = preview ? ` ${theme.fg("dim", preview)}` : "";
|
|
718
|
-
return new Text(`▸ ${theme.fg("toolTitle", theme.bold("workflow"))} ${theme.fg("accent", "inline")}${
|
|
692
|
+
return new Text(`▸ ${theme.fg("toolTitle", theme.bold("workflow"))} ${theme.fg("accent", "inline")}${suffix}${previewSuffix}`, 0, 0);
|
|
719
693
|
},
|
|
720
694
|
renderResult(result, { expanded, isPartial }, theme) {
|
|
721
695
|
if (isPartial) return new Text(theme.fg("accent", "Running workflow…"), 0, 0);
|
|
@@ -731,6 +705,13 @@ function registerWorkflowTool(
|
|
|
731
705
|
return new Text(theme.fg("muted", text), 0, 0);
|
|
732
706
|
},
|
|
733
707
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
708
|
+
if (params.action && params.action !== "start") {
|
|
709
|
+
const { manageWorkflow } = await import("./runtime/workflow-management.ts");
|
|
710
|
+
return await manageWorkflow({ ...params, action: params.action }, ctx, lifecycle, workflowInspectionState(pi, ctx).active);
|
|
711
|
+
}
|
|
712
|
+
if (params.runId !== undefined || params.agentId !== undefined) {
|
|
713
|
+
return { content: [{ type: "text", text: "runId and agentId require inspect or stop." }], details: { error: "invalid_workflow_invocation" } };
|
|
714
|
+
}
|
|
734
715
|
const request = normalizeWorkflowToolRequest(params);
|
|
735
716
|
if (request.kind === "error") return invalidWorkflowInvocationResult();
|
|
736
717
|
const resumeFromRunId = params.resumeFromRunId?.trim();
|
|
@@ -746,13 +727,10 @@ function registerWorkflowTool(
|
|
|
746
727
|
details: { error: "invalid_edited_workflow_resume" },
|
|
747
728
|
};
|
|
748
729
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
if (unavailable) return unavailable;
|
|
752
|
-
}
|
|
730
|
+
const unavailable = workflowUnavailableResult(ctx.mode);
|
|
731
|
+
if (unavailable) return unavailable;
|
|
753
732
|
|
|
754
733
|
const runOptions = resolveWorkflowRunOptions({
|
|
755
|
-
inspect: ctx.hasUI && ctx.mode === "tui",
|
|
756
734
|
concurrency: params.concurrency,
|
|
757
735
|
parallelSubmissionLimit: params.parallelSubmissionLimit,
|
|
758
736
|
maxAgents: params.maxAgents,
|
|
@@ -796,41 +774,15 @@ function registerWorkflowTool(
|
|
|
796
774
|
}
|
|
797
775
|
|
|
798
776
|
const resultArgs = params.args ?? "";
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
options
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
backgroundCtx,
|
|
809
|
-
resultName,
|
|
810
|
-
mod,
|
|
811
|
-
resultArgs,
|
|
812
|
-
backgroundOptions,
|
|
813
|
-
perfRecorder,
|
|
814
|
-
);
|
|
815
|
-
reviewSessions.remember(ctx, execution, backgroundOptions);
|
|
816
|
-
},
|
|
817
|
-
});
|
|
818
|
-
}
|
|
819
|
-
const execution = await executeResolvedWorkflow(pi, ctx, resultName, mod, resultArgs, runOptions, perfRecorder);
|
|
820
|
-
reviewSessions.remember(ctx, execution, runOptions);
|
|
821
|
-
return {
|
|
822
|
-
content: [{
|
|
823
|
-
type: "text",
|
|
824
|
-
text: formatMessageContent(
|
|
825
|
-
resultName,
|
|
826
|
-
execution.envelope.result,
|
|
827
|
-
execution.envelope.usage,
|
|
828
|
-
execution.envelope.perf,
|
|
829
|
-
execution.metadata,
|
|
830
|
-
),
|
|
831
|
-
}],
|
|
832
|
-
details: execution.envelope,
|
|
833
|
-
};
|
|
777
|
+
return await lifecycle.launch({
|
|
778
|
+
ctx,
|
|
779
|
+
name: resultName,
|
|
780
|
+
options: runOptions,
|
|
781
|
+
async execute(runCtx, options) {
|
|
782
|
+
const execution = await executeResolvedWorkflow(pi, runCtx, resultName, mod, resultArgs, options, perfRecorder);
|
|
783
|
+
reviewSessions.remember(ctx, execution, options);
|
|
784
|
+
},
|
|
785
|
+
});
|
|
834
786
|
},
|
|
835
787
|
});
|
|
836
788
|
}
|
|
@@ -41,7 +41,7 @@ export function needsChallenge(finding: AdvisoryVerified): boolean {
|
|
|
41
41
|
|
|
42
42
|
/** A bounded recipe using existing agent/parallel calls, not a new runtime primitive. */
|
|
43
43
|
export async function challengeFindings<T extends AdvisoryVerified>(
|
|
44
|
-
api: Pick<WorkflowApi, "agent" | "parallel">,
|
|
44
|
+
api: Pick<WorkflowApi, "agent" | "modelProfile" | "parallel">,
|
|
45
45
|
findings: T[],
|
|
46
46
|
context: string,
|
|
47
47
|
options: AdvisoryChallengeOptions,
|
|
@@ -56,12 +56,12 @@ export async function challengeFindings<T extends AdvisoryVerified>(
|
|
|
56
56
|
replacements.set(finding.candidateId, { ...finding, verdict: "NOT_SUBSTANTIATED", challenge: { status: "failed" } });
|
|
57
57
|
const challenge = await api.agent(
|
|
58
58
|
`Assume this finding is a false positive. Try to DISPROVE it. Find the strongest concrete counterexample or alternative root cause. Inspect callers, invariants, tests and control flow. For a repair, seek an input, race or error path that still fails. State the smallest experiment distinguishing explanations. Do not edit files or claim tests you did not run. No counterexample found is not proof.\n\nExact review context:\n${context}\n\nCandidate and verifier evidence:\n${JSON.stringify(finding)}`,
|
|
59
|
-
{ label: `challenge:${finding.candidateId}`, phase: "Challenge",
|
|
59
|
+
{ label: `challenge:${finding.candidateId}`, phase: "Challenge", ...api.modelProfile("medium"), tools: DEFAULT_ADVISORY_TOOLS, toolHints: DEFAULT_ADVISORY_TOOL_HINTS, schema: ChallengeSchema },
|
|
60
60
|
);
|
|
61
61
|
replacements.set(finding.candidateId, { ...finding, verdict: "NOT_SUBSTANTIATED", challenge: { status: "failed", challenge } });
|
|
62
62
|
const adjudication = await api.agent(
|
|
63
63
|
`Adjudicate the original finding, independent verifier evidence and falsification attempt below. Preserve unresolved conflict; do not force consensus. A missing counterexample alone cannot upgrade a plausible claim. Cite concrete evidence and observed test results; never invent experiments.\nContext:\n${context}\nOriginal and verifier:\n${JSON.stringify(finding)}\nChallenger:\n${JSON.stringify(challenge)}`,
|
|
64
|
-
{ label: `adjudicate:${finding.candidateId}`, phase: "Challenge",
|
|
64
|
+
{ label: `adjudicate:${finding.candidateId}`, phase: "Challenge", ...api.modelProfile("medium"), tools: [], schema: AdjudicationSchema },
|
|
65
65
|
);
|
|
66
66
|
replacements.set(finding.candidateId, {
|
|
67
67
|
...finding,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
2
|
import { assertWorkflowBudgetAvailable } from "./budget.ts";
|
|
3
|
-
import { isWorkflowPauseError } from "./cancellation.ts";
|
|
3
|
+
import { isWorkflowPauseError, WorkflowAgentStoppedError } from "./cancellation.ts";
|
|
4
4
|
import type { WorkflowAgentReservation } from "./agent-limits.ts";
|
|
5
5
|
import {
|
|
6
6
|
type AgentExecutionOptions,
|
|
@@ -132,7 +132,7 @@ export async function executeAgentAttempt(input: {
|
|
|
132
132
|
tags,
|
|
133
133
|
});
|
|
134
134
|
const result = await workspace.wrapResult(rawResult);
|
|
135
|
-
if (!identity) return { kind: "live-unrecordable", result };
|
|
135
|
+
if (!identity || handle.interacted) return { kind: "live-unrecordable", result };
|
|
136
136
|
if (!isReplayEnabled(replay) || !evidence) throw new Error("Replay identity produced without complete replay evidence.");
|
|
137
137
|
|
|
138
138
|
const contract = await validateReplayIdentity({
|
|
@@ -156,7 +156,7 @@ export async function executeAgentAttempt(input: {
|
|
|
156
156
|
if (
|
|
157
157
|
workspace?.kind === "isolated"
|
|
158
158
|
&& liveStarted
|
|
159
|
-
&& !rc.signal?.aborted
|
|
159
|
+
&& (!rc.signal?.aborted || rc.signal.reason instanceof WorkflowAgentStoppedError)
|
|
160
160
|
&& !isWorkflowPauseError(error)
|
|
161
161
|
) {
|
|
162
162
|
rc.worktrees.preserve(workspace.cwd);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { AgentOptions } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export const WORKFLOW_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const satisfies readonly ThinkingLevel[];
|
|
5
|
+
|
|
6
|
+
export function isWorkflowThinkingLevel(value: string): value is ThinkingLevel {
|
|
7
|
+
return (WORKFLOW_THINKING_LEVELS as readonly string[]).includes(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Validate before admission or any model session can be created. */
|
|
11
|
+
export function assertAgentOptions(options: unknown): asserts options is AgentOptions {
|
|
12
|
+
const opts = options as Partial<AgentOptions> | undefined;
|
|
13
|
+
if (!opts || typeof opts.label !== "string" || !opts.label.trim()
|
|
14
|
+
|| typeof opts.model !== "string" || !opts.model.trim()
|
|
15
|
+
|| typeof opts.thinkingLevel !== "string" || !isWorkflowThinkingLevel(opts.thinkingLevel)) {
|
|
16
|
+
throw new Error("Every agent() requires explicit label, model, and thinkingLevel.");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -13,6 +13,7 @@ import type { WorkflowJournal } from "./journal.ts";
|
|
|
13
13
|
import type { PerfSink } from "./perf.ts";
|
|
14
14
|
import type { AgentOptions, WorkflowProgressEvent } from "./types.ts";
|
|
15
15
|
import type { AgentChatRole } from "./progress-types.ts";
|
|
16
|
+
import type { AgentTranscript } from "./live-agent.ts";
|
|
16
17
|
import type { WorkflowUsageSink } from "./usage.ts";
|
|
17
18
|
import type { WorktreeBaseline, WorktreeRegistry } from "./worktree.ts";
|
|
18
19
|
|
|
@@ -34,16 +35,19 @@ export type AgentRunnerSession = Pick<
|
|
|
34
35
|
| "getLastAssistantText"
|
|
35
36
|
| "isStreaming"
|
|
36
37
|
| "followUp"
|
|
37
|
-
|
|
38
|
+
> & Partial<Pick<AgentSession, "setModel" | "setThinkingLevel" | "steer" | "getSteeringMessages" | "getFollowUpMessages">>;
|
|
38
39
|
|
|
39
40
|
export type CreateAgentSession = (options: CreateAgentSessionOptions) => Promise<{ session: AgentRunnerSession }>;
|
|
40
41
|
|
|
41
42
|
export interface AgentProgress {
|
|
42
|
-
agentQueued(phase: string | undefined, label: string, model?: string): number;
|
|
43
|
+
agentQueued(phase: string | undefined, label: string, model?: string, modelName?: string, thinkingLevel?: string): number;
|
|
44
|
+
bindAgentStop?(id: number, stop: () => void): () => void;
|
|
43
45
|
agentStart(phase: string | undefined, label: string, id?: number, model?: string): void;
|
|
44
46
|
agentTool(label: string, tool: string, id?: number): void;
|
|
45
47
|
agentMessage(id: number, role: AgentChatRole, text: string): void;
|
|
46
|
-
bindAgentFollowUp(id: number, send: (message: string) => Promise<void>): () => void;
|
|
48
|
+
bindAgentFollowUp(id: number, send: (message: string, steer?: boolean) => Promise<void>): () => void;
|
|
49
|
+
bindAgentTranscript?(id: number, read: () => AgentTranscript): () => void;
|
|
50
|
+
agentChanged?(id: number, model?: string, modelName?: string, thinkingLevel?: string): void;
|
|
47
51
|
agentDone(label: string, id?: number): void;
|
|
48
52
|
agentFailed(label: string, error: unknown, id?: number): void;
|
|
49
53
|
event(event: WorkflowProgressEvent): void;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { assertAgentOptions } from "./agent-options.ts";
|
|
1
2
|
import { assertWorkflowBudgetAvailable } from "./budget.ts";
|
|
2
3
|
import { combinedAgentAttemptError } from "./agent-failure.ts";
|
|
3
4
|
import { WorkflowAgentTimeoutError } from "./agent-limits.ts";
|
|
4
|
-
import { abortReason, linkAbortSignal, throwIfAborted } from "./cancellation.ts";
|
|
5
|
+
import { abortReason, linkAbortSignal, throwIfAborted, WorkflowAgentStoppedError } from "./cancellation.ts";
|
|
5
6
|
import { executeAgentAttempt } from "./agent-attempt.ts";
|
|
6
7
|
import {
|
|
7
8
|
createAgentReplayPlan,
|
|
@@ -53,7 +54,8 @@ export async function runAgent(
|
|
|
53
54
|
throw new Error(`agent() prompt must be a string; received ${describeAgentPrompt(prompt)}`);
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
assertAgentOptions(opts);
|
|
58
|
+
const label = opts.label;
|
|
57
59
|
const phase = opts.phase ?? "Workflow";
|
|
58
60
|
const tags: AgentRunTags = { label, phase };
|
|
59
61
|
|
|
@@ -62,14 +64,15 @@ export async function runAgent(
|
|
|
62
64
|
const routing = resolveAgentRouting(rc, opts, label);
|
|
63
65
|
const effectiveOpts = routing.thinkingLevel === opts.thinkingLevel
|
|
64
66
|
? opts
|
|
65
|
-
: { ...opts, thinkingLevel: routing.thinkingLevel };
|
|
67
|
+
: { ...opts, thinkingLevel: routing.thinkingLevel ?? opts.thinkingLevel };
|
|
66
68
|
const replay = createAgentReplayPlan(prompt, effectiveOpts);
|
|
67
69
|
if (!isReplayEnabled(replay)) assertWorkflowBudgetAvailable(rc.budget);
|
|
68
70
|
|
|
69
71
|
const modelLabel = describeAgentModel(routing.model);
|
|
70
|
-
const rowId = rc.progress.agentQueued(opts.phase, label, modelLabel);
|
|
72
|
+
const rowId = rc.progress.agentQueued(opts.phase, label, modelLabel, routing.model?.name, effectiveOpts.thinkingLevel);
|
|
71
73
|
rc.progress.agentMessage(rowId, "task", prompt);
|
|
72
74
|
const liveScope = createAgentLiveScope(rc, label);
|
|
75
|
+
const unbindStop = rc.progress.bindAgentStop?.(rowId, liveScope.stop);
|
|
73
76
|
try {
|
|
74
77
|
return await rc.semaphore.run(
|
|
75
78
|
async () => {
|
|
@@ -129,18 +132,20 @@ export async function runAgent(
|
|
|
129
132
|
}
|
|
130
133
|
continue;
|
|
131
134
|
}
|
|
135
|
+
throwIfAborted(agentRc.signal);
|
|
132
136
|
const settlement = await settleAgentAttempt({ rc: agentRc, label, tags, replay: attemptPlan, outcome });
|
|
133
137
|
if (settlement.kind === "retry-live") {
|
|
134
138
|
attemptPlan = { kind: "off" };
|
|
135
139
|
continue;
|
|
136
140
|
}
|
|
141
|
+
throwIfAborted(agentRc.signal);
|
|
137
142
|
rc.progress.agentDone(label, rowId);
|
|
138
143
|
return settlement.result;
|
|
139
144
|
}
|
|
140
145
|
},
|
|
141
146
|
{
|
|
142
147
|
onQueueWaitMs: (durationMs) => rc.perf.observe("agent.queue_wait_ms", durationMs, tags),
|
|
143
|
-
signal:
|
|
148
|
+
signal: liveScope.signal,
|
|
144
149
|
},
|
|
145
150
|
);
|
|
146
151
|
} catch (error) {
|
|
@@ -151,6 +156,7 @@ export async function runAgent(
|
|
|
151
156
|
rc.progress.log(`${label} failed: ${unknownErrorMessage(failure)}`);
|
|
152
157
|
throw failure;
|
|
153
158
|
} finally {
|
|
159
|
+
unbindStop?.();
|
|
154
160
|
liveScope.dispose();
|
|
155
161
|
}
|
|
156
162
|
}, tags);
|
|
@@ -164,6 +170,8 @@ function createAgentLiveScope(rc: RunContext, label: string) {
|
|
|
164
170
|
|
|
165
171
|
return {
|
|
166
172
|
signal: controller.signal,
|
|
173
|
+
// A local stop is recoverable by parallel(); it must not abort sibling agents.
|
|
174
|
+
stop: () => controller.abort(new WorkflowAgentStoppedError(`Agent ${label} stopped by user.`)),
|
|
167
175
|
reserve() {
|
|
168
176
|
const reservation = rc.agentLimiter.reserve(controller.signal);
|
|
169
177
|
let committed = false;
|
|
@@ -194,7 +202,7 @@ function resolveAgentRouting(
|
|
|
194
202
|
rc: RunContext,
|
|
195
203
|
opts: AgentExecutionOptions,
|
|
196
204
|
label: string,
|
|
197
|
-
): { readonly model: ResolvedAgentModel["model"]; readonly thinkingLevel: AgentExecutionOptions["thinkingLevel"] } {
|
|
205
|
+
): { readonly model: ResolvedAgentModel["model"]; readonly thinkingLevel: AgentExecutionOptions["thinkingLevel"] | undefined } {
|
|
198
206
|
try {
|
|
199
207
|
return resolveAgentModelProfile(
|
|
200
208
|
{
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { sendAgentInput, type AgentTranscript } from "./live-agent.ts";
|
|
1
3
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
4
|
import {
|
|
3
5
|
createAgentSessionFromServices,
|
|
@@ -45,7 +47,9 @@ export interface ResolvedAgentModel {
|
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
export interface AgentSessionHandle {
|
|
50
|
+
interacted?: boolean;
|
|
48
51
|
readonly session: AgentRunnerSession;
|
|
52
|
+
readonly cwd?: string;
|
|
49
53
|
readonly selectedSkills: readonly Skill[];
|
|
50
54
|
hasStructuredResult(): boolean;
|
|
51
55
|
structuredResult(): unknown;
|
|
@@ -124,6 +128,7 @@ export async function openAgentSession(input: {
|
|
|
124
128
|
throwIfAborted(rc.signal);
|
|
125
129
|
return {
|
|
126
130
|
session,
|
|
131
|
+
cwd,
|
|
127
132
|
selectedSkills: resources.selectedSkills,
|
|
128
133
|
hasStructuredResult: () => captured,
|
|
129
134
|
structuredResult: () => structuredResult,
|
|
@@ -149,13 +154,38 @@ export async function promptAgentSession(input: {
|
|
|
149
154
|
}): Promise<unknown> {
|
|
150
155
|
const { rc, handle, prompt, opts, label, rowId, tags } = input;
|
|
151
156
|
const { session } = handle;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
157
|
+
let streaming: AgentMessage | undefined;
|
|
158
|
+
const toolUpdates = new Map<string, NonNullable<AgentTranscript["toolUpdates"]> extends ReadonlyMap<string, infer V> ? V : never>();
|
|
159
|
+
const refresh = () => rc.progress.agentChanged?.(rowId,
|
|
160
|
+
session.model ? `${session.model.provider}/${session.model.id}` : undefined,
|
|
161
|
+
session.model?.name, session.thinkingLevel);
|
|
162
|
+
const unbindTranscript = rc.progress.bindAgentTranscript?.(rowId, () => ({
|
|
163
|
+
messages: session.messages, streaming, cwd: handle.cwd, toolUpdates,
|
|
164
|
+
steering: session.getSteeringMessages?.() ?? [],
|
|
165
|
+
followUp: session.getFollowUpMessages?.() ?? [],
|
|
166
|
+
}));
|
|
167
|
+
const unbindFollowUp = rc.progress.bindAgentFollowUp(rowId, async (message, steer) => {
|
|
168
|
+
await sendAgentInput(session, rc, message, steer);
|
|
169
|
+
handle.interacted = true;
|
|
170
|
+
refresh();
|
|
155
171
|
});
|
|
156
172
|
const unsubscribe = session.subscribe((event) => {
|
|
173
|
+
if (event.type === "message_update" || event.type === "message_start") streaming = event.message;
|
|
174
|
+
if (event.type === "message_end") streaming = undefined;
|
|
175
|
+
if (event.type === "tool_execution_update") {
|
|
176
|
+
toolUpdates.set(event.toolCallId, { result: event.partialResult, isPartial: true, isError: false });
|
|
177
|
+
} else if (event.type === "tool_execution_end") {
|
|
178
|
+
toolUpdates.set(event.toolCallId, { result: event.result, isPartial: false, isError: event.isError });
|
|
179
|
+
}
|
|
180
|
+
refresh();
|
|
181
|
+
if (event.type === "tool_execution_end") {
|
|
182
|
+
const output = event.result.content.filter((part: { type: string }) => part.type === "text")
|
|
183
|
+
.map((part: { text?: string }) => part.text ?? "").join("\n");
|
|
184
|
+
rc.progress.agentMessage(rowId, "tool", `${event.toolName}: ${output}`);
|
|
185
|
+
}
|
|
157
186
|
if (event.type === "tool_execution_start" && event.toolName !== undefined && event.toolName !== FINAL_TOOL) {
|
|
158
187
|
rc.progress.agentTool(label, event.toolName, rowId);
|
|
188
|
+
rc.progress.agentMessage(rowId, "tool", `${event.toolName} ${JSON.stringify(event.args)}`);
|
|
159
189
|
return;
|
|
160
190
|
}
|
|
161
191
|
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
@@ -217,6 +247,7 @@ export async function promptAgentSession(input: {
|
|
|
217
247
|
unlinkPromptAbort();
|
|
218
248
|
}
|
|
219
249
|
} finally {
|
|
250
|
+
unbindTranscript?.();
|
|
220
251
|
unbindFollowUp();
|
|
221
252
|
unsubscribe();
|
|
222
253
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/** Agent-local cancellation is recoverable and must not stop sibling tasks. */
|
|
2
|
+
export class WorkflowAgentStoppedError extends Error {
|
|
3
|
+
override readonly name = "WorkflowAgentStoppedError";
|
|
4
|
+
}
|
|
5
|
+
|
|
1
6
|
export class WorkflowAbortError extends Error {
|
|
2
7
|
constructor(message = "Workflow aborted") {
|
|
3
8
|
super(message);
|