@oh-my-pi/pi-coding-agent 17.0.5 → 17.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +172 -0
- package/dist/cli.js +2838 -2917
- package/dist/types/exec/bash-executor.d.ts +1 -0
- package/dist/types/extensibility/extensions/load-errors.d.ts +3 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +8 -0
- package/dist/types/modes/components/status-line/component.d.ts +8 -0
- package/dist/types/modes/components/usage-row.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-client.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-frame.d.ts +9 -0
- package/dist/types/modes/setup-wizard/index.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/model.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +23 -5
- package/dist/types/session/session-paths.d.ts +21 -4
- package/dist/types/tools/xdev.d.ts +5 -3
- package/dist/types/vibe/__tests__/token-rate.test.d.ts +1 -0
- package/dist/types/vibe/runtime.d.ts +23 -0
- package/package.json +12 -12
- package/src/config/model-discovery.ts +3 -2
- package/src/config/model-registry.ts +6 -2
- package/src/exec/bash-executor.ts +68 -8
- package/src/extensibility/extensions/load-errors.ts +13 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +41 -6
- package/src/main.ts +8 -0
- package/src/modes/components/chat-transcript-builder.ts +10 -1
- package/src/modes/components/status-line/component.ts +55 -1
- package/src/modes/components/usage-row.ts +17 -1
- package/src/modes/controllers/command-controller.ts +41 -1
- package/src/modes/controllers/event-controller.ts +6 -1
- package/src/modes/interactive-mode.ts +62 -52
- package/src/modes/noninteractive-dispose.test.ts +2 -0
- package/src/modes/print-mode.ts +60 -0
- package/src/modes/rpc/rpc-client.ts +76 -35
- package/src/modes/rpc/rpc-frame.ts +156 -0
- package/src/modes/rpc/rpc-mode.ts +4 -2
- package/src/modes/setup-wizard/index.ts +2 -0
- package/src/modes/setup-wizard/scenes/model.ts +134 -0
- package/src/modes/utils/ui-helpers.ts +6 -1
- package/src/prompts/system/workflow-notice.md +89 -74
- package/src/session/agent-session.ts +89 -26
- package/src/session/session-manager.ts +34 -3
- package/src/session/session-paths.ts +38 -9
- package/src/task/executor.ts +45 -0
- package/src/tools/xdev.ts +11 -4
- package/src/tools/yield.ts +4 -1
- package/src/{modes/components/status-line → utils}/token-rate.ts +6 -0
- package/src/vibe/__tests__/token-rate.test.ts +96 -0
- package/src/vibe/runtime.ts +50 -0
- /package/dist/types/{modes/components/status-line → utils}/token-rate.d.ts +0 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { isRecord } from "@oh-my-pi/pi-utils";
|
|
3
|
+
|
|
4
|
+
/** Maximum UTF-8 size of one newline-delimited RPC frame, including the newline. */
|
|
5
|
+
export const MAX_RPC_FRAME_BYTES = 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
interface ShrinkPass {
|
|
8
|
+
stringCap: number;
|
|
9
|
+
arrayLimit: number;
|
|
10
|
+
objectLimit: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const SHRINK_PASSES: readonly ShrinkPass[] = [
|
|
14
|
+
{ stringCap: 256 * 1024, arrayLimit: 512, objectLimit: 512 },
|
|
15
|
+
{ stringCap: 64 * 1024, arrayLimit: 256, objectLimit: 256 },
|
|
16
|
+
{ stringCap: 16 * 1024, arrayLimit: 128, objectLimit: 128 },
|
|
17
|
+
{ stringCap: 4 * 1024, arrayLimit: 64, objectLimit: 64 },
|
|
18
|
+
{ stringCap: 1024, arrayLimit: 32, objectLimit: 32 },
|
|
19
|
+
{ stringCap: 256, arrayLimit: 8, objectLimit: 16 },
|
|
20
|
+
{ stringCap: 64, arrayLimit: 1, objectLimit: 8 },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const STRING_ELISION_RESERVE = 80;
|
|
24
|
+
const METADATA_STRING_CAP = 1024;
|
|
25
|
+
|
|
26
|
+
function serializedFrameBytes(json: string): number {
|
|
27
|
+
return Buffer.byteLength(json, "utf8") + 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function shrinkString(value: string, cap: number): string {
|
|
31
|
+
if (value.length <= cap) return value;
|
|
32
|
+
const headLength = Math.max(0, cap - STRING_ELISION_RESERVE);
|
|
33
|
+
return `${value.slice(0, headLength)}\n…[${value.length - headLength} chars elided for RPC frame]`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function shrinkValue(value: unknown, pass: ShrinkPass): unknown {
|
|
37
|
+
if (typeof value === "string") return shrinkString(value, pass.stringCap);
|
|
38
|
+
if (Array.isArray(value)) {
|
|
39
|
+
const keep = Math.min(value.length, pass.arrayLimit);
|
|
40
|
+
const output: unknown[] = new Array(keep + (keep < value.length ? 1 : 0));
|
|
41
|
+
for (let index = 0; index < keep; index++) output[index] = shrinkValue(value[index], pass);
|
|
42
|
+
if (keep < value.length) output[keep] = `…[${value.length - keep} items elided for RPC frame]`;
|
|
43
|
+
return output;
|
|
44
|
+
}
|
|
45
|
+
if (isRecord(value)) {
|
|
46
|
+
const entries = Object.entries(value);
|
|
47
|
+
const keep = Math.min(entries.length, pass.objectLimit);
|
|
48
|
+
const output: Record<string, unknown> = {};
|
|
49
|
+
for (let index = 0; index < keep; index++) {
|
|
50
|
+
const [key, item] = entries[index];
|
|
51
|
+
output[key] = shrinkValue(item, pass);
|
|
52
|
+
}
|
|
53
|
+
if (keep < entries.length) output.rpcFrameElidedKeys = entries.length - keep;
|
|
54
|
+
return output;
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function jsonSnapshot(value: unknown): unknown {
|
|
60
|
+
const json = JSON.stringify(value);
|
|
61
|
+
return json === undefined ? undefined : JSON.parse(json);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function encodedMessageSnapshot(encoded: string): { message: unknown } | undefined {
|
|
65
|
+
const frame = JSON.parse(encoded);
|
|
66
|
+
return isRecord(frame) && frame.type === "message_end" && Object.hasOwn(frame, "message")
|
|
67
|
+
? { message: frame.message }
|
|
68
|
+
: undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function compactTerminalFrame(
|
|
72
|
+
frame: object,
|
|
73
|
+
streamedMessageCount: number,
|
|
74
|
+
streamedMessages?: readonly unknown[],
|
|
75
|
+
): object {
|
|
76
|
+
if (!isRecord(frame) || frame.type !== "agent_end" || !Array.isArray(frame.messages)) return frame;
|
|
77
|
+
let streamed = Number.isSafeInteger(streamedMessageCount)
|
|
78
|
+
? Math.min(Math.max(0, streamedMessageCount), frame.messages.length)
|
|
79
|
+
: 0;
|
|
80
|
+
if (streamedMessages) {
|
|
81
|
+
streamed = 0;
|
|
82
|
+
const limit = Math.min(streamedMessages.length, frame.messages.length);
|
|
83
|
+
while (
|
|
84
|
+
streamed < limit &&
|
|
85
|
+
isDeepStrictEqual(streamedMessages[streamed], jsonSnapshot(frame.messages[streamed]))
|
|
86
|
+
) {
|
|
87
|
+
streamed++;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
...frame,
|
|
92
|
+
messages: frame.messages.slice(streamed),
|
|
93
|
+
messageCount: frame.messages.length,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function overflowFrame(frame: object): object {
|
|
98
|
+
if (!isRecord(frame)) return { type: "rpc_frame_error", error: "RPC frame exceeded the transport limit" };
|
|
99
|
+
if (frame.type === "response") {
|
|
100
|
+
return {
|
|
101
|
+
id: typeof frame.id === "string" ? shrinkString(frame.id, METADATA_STRING_CAP) : undefined,
|
|
102
|
+
type: "response",
|
|
103
|
+
command: typeof frame.command === "string" ? shrinkString(frame.command, METADATA_STRING_CAP) : "unknown",
|
|
104
|
+
success: false,
|
|
105
|
+
error: "RPC response exceeded the transport limit",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (frame.type === "agent_end") {
|
|
109
|
+
return {
|
|
110
|
+
type: "agent_end",
|
|
111
|
+
messages: [],
|
|
112
|
+
messageCount: typeof frame.messageCount === "number" ? frame.messageCount : 0,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
type: "rpc_frame_error",
|
|
117
|
+
originalType: typeof frame.type === "string" ? shrinkString(frame.type, METADATA_STRING_CAP) : undefined,
|
|
118
|
+
error: "RPC frame exceeded the transport limit",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Serialize a complete JSONL frame while enforcing the transport byte ceiling. */
|
|
123
|
+
export function encodeRpcFrame(frame: object, streamedMessageCount = 0, streamedMessages?: readonly unknown[]): string {
|
|
124
|
+
let json = JSON.stringify(frame);
|
|
125
|
+
if (serializedFrameBytes(json) <= MAX_RPC_FRAME_BYTES) return `${json}\n`;
|
|
126
|
+
if (isRecord(frame) && frame.type === "response") {
|
|
127
|
+
return `${JSON.stringify(overflowFrame(frame))}\n`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const compacted = compactTerminalFrame(frame, streamedMessageCount, streamedMessages);
|
|
131
|
+
json = JSON.stringify(compacted);
|
|
132
|
+
if (serializedFrameBytes(json) <= MAX_RPC_FRAME_BYTES) return `${json}\n`;
|
|
133
|
+
|
|
134
|
+
for (const pass of SHRINK_PASSES) {
|
|
135
|
+
json = JSON.stringify(shrinkValue(compacted, pass));
|
|
136
|
+
if (serializedFrameBytes(json) <= MAX_RPC_FRAME_BYTES) return `${json}\n`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return `${JSON.stringify(overflowFrame(compacted))}\n`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Stateful encoder that tracks which messages a client has already received. */
|
|
143
|
+
export class RpcFrameEncoder {
|
|
144
|
+
#streamedMessages: unknown[] = [];
|
|
145
|
+
|
|
146
|
+
encode(frame: object): string {
|
|
147
|
+
if (isRecord(frame) && frame.type === "agent_start") this.#streamedMessages = [];
|
|
148
|
+
const encoded = encodeRpcFrame(frame, this.#streamedMessages.length, this.#streamedMessages);
|
|
149
|
+
if (!isRecord(frame)) return encoded;
|
|
150
|
+
if (frame.type === "message_end") {
|
|
151
|
+
const snapshot = encodedMessageSnapshot(encoded);
|
|
152
|
+
if (snapshot) this.#streamedMessages.push(snapshot.message);
|
|
153
|
+
} else if (frame.type === "agent_end" && frame.willContinue !== true) this.#streamedMessages = [];
|
|
154
|
+
return encoded;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -34,6 +34,7 @@ import type { EventBus } from "../../utils/event-bus";
|
|
|
34
34
|
import { initializeExtensions } from "../runtime-init";
|
|
35
35
|
import { isRpcHostToolResult, isRpcHostToolUpdate, RpcHostToolBridge } from "./host-tools";
|
|
36
36
|
import { isRpcHostUriResult, RpcHostUriBridge } from "./host-uris";
|
|
37
|
+
import { RpcFrameEncoder } from "./rpc-frame";
|
|
37
38
|
import { claimRpcInput } from "./rpc-input";
|
|
38
39
|
import { RpcSubagentRegistry, readRpcSubagentTranscript } from "./rpc-subagents";
|
|
39
40
|
import type {
|
|
@@ -617,9 +618,10 @@ export async function runRpcMode(
|
|
|
617
618
|
// may write there.
|
|
618
619
|
process.env.PI_NOTIFICATIONS = "off";
|
|
619
620
|
|
|
620
|
-
|
|
621
|
+
const frameEncoder = new RpcFrameEncoder();
|
|
622
|
+
process.stdout.write(frameEncoder.encode({ type: "ready" }));
|
|
621
623
|
const output = (obj: RpcResponse | RpcExtensionUIRequest | object) => {
|
|
622
|
-
process.stdout.write(
|
|
624
|
+
process.stdout.write(frameEncoder.encode(obj));
|
|
623
625
|
};
|
|
624
626
|
const emitRpcTitles = shouldEmitRpcTitles();
|
|
625
627
|
|
|
@@ -2,6 +2,7 @@ import type { Settings } from "../../config/settings";
|
|
|
2
2
|
import { CURRENT_SETUP_VERSION } from "../setup-version";
|
|
3
3
|
import type { InteractiveModeContext } from "../types";
|
|
4
4
|
import { glyphSetupScene } from "./scenes/glyph";
|
|
5
|
+
import { modelSetupScene } from "./scenes/model";
|
|
5
6
|
import { providersSetupScene } from "./scenes/providers";
|
|
6
7
|
import { themeSetupScene } from "./scenes/theme";
|
|
7
8
|
import type { SetupScene } from "./scenes/types";
|
|
@@ -14,6 +15,7 @@ export { CURRENT_SETUP_VERSION };
|
|
|
14
15
|
|
|
15
16
|
export const ALL_SCENES = [
|
|
16
17
|
providersSetupScene,
|
|
18
|
+
modelSetupScene,
|
|
17
19
|
glyphSetupScene,
|
|
18
20
|
themeSetupScene,
|
|
19
21
|
] as const satisfies readonly SetupScene[];
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { Model } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import type { SgrMouseEvent } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import {
|
|
4
|
+
buildBrowserItems,
|
|
5
|
+
ModelBrowser,
|
|
6
|
+
resolveRoleAssignments,
|
|
7
|
+
sortModelItems,
|
|
8
|
+
} from "../../components/model-browser";
|
|
9
|
+
import { theme } from "../../theme/theme";
|
|
10
|
+
import type { SetupScene, SetupSceneController, SetupSceneHost } from "./types";
|
|
11
|
+
|
|
12
|
+
const MAX_VISIBLE_MODELS = 10;
|
|
13
|
+
const WIZARD_SCREEN_RESERVE = 22;
|
|
14
|
+
|
|
15
|
+
class ModelSceneController implements SetupSceneController {
|
|
16
|
+
title = "Choose your default model";
|
|
17
|
+
subtitle = "Search configured models and save the model used for new sessions.";
|
|
18
|
+
#browser: ModelBrowser;
|
|
19
|
+
#status: string | undefined;
|
|
20
|
+
#selecting = false;
|
|
21
|
+
#disposed = false;
|
|
22
|
+
#browserRowStart = 2;
|
|
23
|
+
|
|
24
|
+
constructor(private readonly host: SetupSceneHost) {
|
|
25
|
+
this.#browser = new ModelBrowser(host.ctx.settings);
|
|
26
|
+
this.#browser.onActivate = item => {
|
|
27
|
+
void this.#select(item.model, item.selector);
|
|
28
|
+
};
|
|
29
|
+
this.#browser.onCancel = () => host.finish("skipped");
|
|
30
|
+
this.#syncModels();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async onMount(): Promise<void> {
|
|
34
|
+
this.#status = theme.fg("muted", "Discovering available models…");
|
|
35
|
+
this.host.requestRender();
|
|
36
|
+
await this.#refreshModels();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
dispose(): void {
|
|
40
|
+
this.#disposed = true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
invalidate(): void {
|
|
44
|
+
this.#browser.invalidate();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
handleInput(data: string): void {
|
|
48
|
+
if (this.#selecting) return;
|
|
49
|
+
this.#browser.handleInput(data);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
routeMouse(event: SgrMouseEvent, line: number): void {
|
|
53
|
+
if (this.#selecting) return;
|
|
54
|
+
this.#browser.routeMouse(event, line - this.#browserRowStart);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
render(width: number): readonly string[] {
|
|
58
|
+
const visibleRows = Math.max(
|
|
59
|
+
1,
|
|
60
|
+
Math.min(MAX_VISIBLE_MODELS, this.host.ctx.ui.terminal.rows - WIZARD_SCREEN_RESERVE),
|
|
61
|
+
);
|
|
62
|
+
this.#browser.setMaxVisible(visibleRows);
|
|
63
|
+
const lines = [
|
|
64
|
+
this.#status ?? theme.fg("muted", "Type to search. Enter saves the highlighted model as your default."),
|
|
65
|
+
"",
|
|
66
|
+
];
|
|
67
|
+
this.#browserRowStart = lines.length;
|
|
68
|
+
lines.push(...this.#browser.render(width));
|
|
69
|
+
return lines;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#syncModels(): void {
|
|
73
|
+
const registry = this.host.ctx.session.modelRegistry;
|
|
74
|
+
const available = registry.getAvailable();
|
|
75
|
+
const roles = resolveRoleAssignments(this.host.ctx.settings, registry.getAll(), available);
|
|
76
|
+
const storage = this.host.ctx.settings.getStorage();
|
|
77
|
+
const items = buildBrowserItems(available);
|
|
78
|
+
sortModelItems(items, { roles, mruOrder: storage?.getModelUsageOrder() ?? [] });
|
|
79
|
+
this.#browser.setRoles(roles);
|
|
80
|
+
this.#browser.setMruOrder(storage?.getModelUsageOrder() ?? []);
|
|
81
|
+
this.#browser.setPerfStats(storage?.getModelPerf() ?? new Map());
|
|
82
|
+
this.#browser.setItems(items);
|
|
83
|
+
|
|
84
|
+
const current = this.host.ctx.session.model;
|
|
85
|
+
if (current) {
|
|
86
|
+
const selector = `${current.provider}/${current.id}`;
|
|
87
|
+
this.#browser.setCurrentSelector(selector);
|
|
88
|
+
this.#browser.selectSelector(selector);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async #refreshModels(): Promise<void> {
|
|
93
|
+
try {
|
|
94
|
+
await this.host.ctx.session.modelRegistry.refresh("online-if-uncached");
|
|
95
|
+
if (this.#disposed) return;
|
|
96
|
+
this.#syncModels();
|
|
97
|
+
this.#status = undefined;
|
|
98
|
+
this.host.requestRender();
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (this.#disposed) return;
|
|
101
|
+
this.#status = theme.fg("error", error instanceof Error ? error.message : String(error));
|
|
102
|
+
this.host.requestRender();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async #select(model: Model, selector: string): Promise<void> {
|
|
107
|
+
if (this.#selecting) return;
|
|
108
|
+
this.#selecting = true;
|
|
109
|
+
this.#status = theme.fg("muted", `Saving ${selector} as the default model…`);
|
|
110
|
+
this.host.requestRender();
|
|
111
|
+
try {
|
|
112
|
+
const projectScope = this.host.ctx.settings.get("modelRoleStorage") === "project";
|
|
113
|
+
await this.host.ctx.session.setModel(model, "default", { selector, persist: !projectScope });
|
|
114
|
+
if (projectScope) {
|
|
115
|
+
this.host.ctx.settings.setProjectModelRole("default", selector);
|
|
116
|
+
}
|
|
117
|
+
await this.host.ctx.settings.flush();
|
|
118
|
+
if (!this.#disposed) this.host.finish("done");
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (this.#disposed) return;
|
|
121
|
+
this.#selecting = false;
|
|
122
|
+
this.#status = theme.fg("error", error instanceof Error ? error.message : String(error));
|
|
123
|
+
this.host.requestRender();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Setup step that assigns one available model to the persisted default role. */
|
|
129
|
+
export const modelSetupScene: SetupScene = {
|
|
130
|
+
id: "model",
|
|
131
|
+
title: "Choose your default model",
|
|
132
|
+
minVersion: 1,
|
|
133
|
+
mount: host => new ModelSceneController(host),
|
|
134
|
+
};
|
|
@@ -301,14 +301,18 @@ export class UiHelpers {
|
|
|
301
301
|
let pendingUsage: Usage | undefined;
|
|
302
302
|
let pendingUsageDuration: number | undefined;
|
|
303
303
|
let pendingUsageTtft: number | undefined;
|
|
304
|
+
let pendingUsageTimestamp: number | undefined;
|
|
304
305
|
const flushPendingUsage = () => {
|
|
305
306
|
if (!pendingUsage) return;
|
|
306
307
|
readGroup?.seal();
|
|
307
308
|
readGroup = null;
|
|
308
|
-
this.ctx.chatContainer.addChild(
|
|
309
|
+
this.ctx.chatContainer.addChild(
|
|
310
|
+
createUsageRowBlock(pendingUsage, pendingUsageDuration, pendingUsageTtft, pendingUsageTimestamp),
|
|
311
|
+
);
|
|
309
312
|
pendingUsage = undefined;
|
|
310
313
|
pendingUsageDuration = undefined;
|
|
311
314
|
pendingUsageTtft = undefined;
|
|
315
|
+
pendingUsageTimestamp = undefined;
|
|
312
316
|
};
|
|
313
317
|
// Rebuild-time mirror of the event controller's displaceable-poll
|
|
314
318
|
// bookkeeping: a `hub` wait that found every watched job still running is
|
|
@@ -509,6 +513,7 @@ export class UiHelpers {
|
|
|
509
513
|
: undefined;
|
|
510
514
|
pendingUsageDuration = message.duration;
|
|
511
515
|
pendingUsageTtft = message.ttft;
|
|
516
|
+
pendingUsageTimestamp = message.timestamp;
|
|
512
517
|
} else if (message.role === "toolResult") {
|
|
513
518
|
const pendingReadComponent = this.ctx.pendingTools.get(message.toolCallId);
|
|
514
519
|
const isReadGroupResult =
|
|
@@ -1,89 +1,104 @@
|
|
|
1
1
|
<system-notice>
|
|
2
|
-
The user's message above contains the **workflowz** keyword: drive this task as a deterministic multi-subagent workflow.
|
|
2
|
+
The user's message above contains the **workflowz** keyword: drive this task as a deterministic multi-subagent workflow. Author the orchestration in the `eval` tool and fan out subagents — to be comprehensive (decompose and cover in parallel), to be confident (independent perspectives and adversarial checks before you commit), or to take on scale one context can't hold (audits, migrations, broad sweeps). This overrides any default tendency to do the whole task inline when fanning out would be more thorough.
|
|
3
3
|
|
|
4
4
|
<when>
|
|
5
|
-
Worth it when the task benefits from decomposition + parallel coverage, or from independent/adversarial cross-checking before you commit. For a quick lookup or single edit, just do it directly — don't spin up agents. Scout inline
|
|
6
|
-
- **Understand** — parallel readers over subsystems → structured map
|
|
7
|
-
- **Design** — independent approaches → scored synthesis
|
|
8
|
-
- **Review** — split dimensions → find per dimension → adversarially verify each finding
|
|
9
|
-
- **Research** — multi-modal sweep → deep-read the hits → synthesize
|
|
10
|
-
- **Migrate** — discover sites → transform each → verify
|
|
5
|
+
Worth it when the task benefits from decomposition + parallel coverage, or from independent/adversarial cross-checking before you commit. For a quick lookup or single edit, just do it directly — don't spin up agents. Scout inline FIRST (list the files, scope the diff, find the call sites) to discover the work-list, then fan out over it — you don't need to know the shape before the *task*, only before the *fan-out*. Common shapes, each a well-scoped `eval` call you can chain across turns:
|
|
6
|
+
- **Understand** — parallel readers over subsystems → structured map
|
|
7
|
+
- **Design** — judge panel of N independent approaches → scored synthesis
|
|
8
|
+
- **Review** — split into dimensions → find per dimension → adversarially verify each finding
|
|
9
|
+
- **Research** — multi-modal sweep → deep-read the hits → synthesize
|
|
10
|
+
- **Migrate** — discover sites → transform each → verify
|
|
11
11
|
</when>
|
|
12
12
|
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
`
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
Each task assignment must be self-contained:
|
|
27
|
-
|
|
28
|
-
# Target
|
|
29
|
-
Exact files, symbols, subsystem, or evidence surface; explicit non-goals.
|
|
30
|
-
# Change
|
|
31
|
-
What to inspect or modify, step by step, including APIs and patterns to reuse.
|
|
32
|
-
# Acceptance
|
|
33
|
-
Observable result, return packet, and local verification. Subagents skip formatters,
|
|
34
|
-
linters, and project-wide tests; the parent runs shared proof once.
|
|
35
|
-
{{else}}
|
|
36
|
-
Call `task` once per independent subagent. Put the full shared background and the leaf work in that call's `assignment`. Do not pass `context` or `tasks[]`: the flat task schema rejects them when batch calls are disabled.
|
|
37
|
-
|
|
38
|
-
Each assignment must be self-contained:
|
|
39
|
-
|
|
40
|
-
# Target
|
|
41
|
-
Exact files, symbols, subsystem, or evidence surface; explicit non-goals.
|
|
42
|
-
# Change
|
|
43
|
-
Shared background plus what to inspect or modify, step by step, including APIs and patterns to reuse.
|
|
44
|
-
# Acceptance
|
|
45
|
-
Observable result, return packet, and local verification. Subagents skip formatters,
|
|
46
|
-
linters, and project-wide tests; the parent runs shared proof once.
|
|
47
|
-
{{/if}}
|
|
13
|
+
<helpers>
|
|
14
|
+
State persists across eval calls, so scout in one call and fan out in the next. Every eval call has:
|
|
15
|
+
|
|
16
|
+
- `agent(prompt, *, agent="task", model=None, label=None, schema=None, isolated=None, apply=None, merge=None, handle=False)` — run ONE subagent; returns its final text, or the validated object when `schema` (a JSON Schema dict) is given. With `schema` the subagent is forced to emit structured output that is validated for you — branch on the object, not on parsed prose. `agent` picks a discovered agent ("explore", "reviewer", …); `label` names the artifact. Shared background goes in a `local://` file referenced from each prompt, not a parameter. Subagents are told their final text IS the return value, so they hand back raw data. `agent()` blocks until the subagent finishes. Recursion follows `task.maxRecursionDepth` (default 2; a negative value disables the cap): main agent depth = 0, each `agent()` child increments depth by 1, and, when the cap is non-negative, a spawner may call `agent()` only while its current `taskDepth < cap`. Pass `isolated=True` to run the spawn in a copy-on-write worktree so parallel `agent()` calls can edit overlapping files safely — strict opt-in, mirrors the `task` tool, defaults off regardless of `task.isolation.mode`; `isolated=True` while the setting is `"none"` errors out instead of silently downgrading. With isolation, `apply=False` keeps changes in the worktree, and `merge=False` forces patch mode even when the setting is `"branch"`. Captured root patch path, branch name, nested repo patches, and apply summary reach the workflow through `handle=True` — combine it with `apply=False` (or `apply=False, schema=…`) and read `node["patch_path"]`, `node["branch_name"]`, `node["nested_patches"]`, `node["changes_applied"]`, `node["isolation_summary"]` (JS: same keys camelCased) to recover artifacts.
|
|
17
|
+
- `parallel(thunks)` — run zero-arg callables concurrently through a bounded pool, preserving input order; returns once all finish. The pool is bounded by the session's `task` concurrency — don't hand-tune it; fan out as wide as the work divides. A thunk that raises propagates — wrap risky work in `try/except` inside the thunk to keep partial results. In a loop, bind each closure's value with a default arg (`lambda d=d: …`) or every thunk captures the last one.
|
|
18
|
+
- `pipeline(items, *stages)` — map items through `stages` left-to-right. There is a BARRIER between stages: ALL items clear stage N before stage N+1 begins. Each stage is a one-arg callable; stage 1 gets the original item, later stages get the previous result. Same pool width as `parallel()`.
|
|
19
|
+
- `completion(prompt, *, model="default", system=None, schema=None)` — oneshot, stateless model call (no tools, no history). Tiers: "smol", "default", "slow". Cheap classification/scoring inside a fan-out.
|
|
20
|
+
- `log(message)` — emit a progress line above the status tree. `phase(title)` — start a phase; the status lines that follow group under it.
|
|
21
|
+
- `budget` — `budget.total` (output-token ceiling, or `None` when none is set), `budget.spent()` (tokens spent this turn — main loop + eval subagents), `budget.remaining()` (`math.inf` when total is `None`), `budget.hard` (whether it's enforced). A ceiling is set by the user: `+Nk` in their message is advisory (you self-limit via `budget.remaining()`), `+Nk!` (or Goal Mode) is hard — `agent()` refuses to spawn once spent reaches it. Gate loops on `budget.total` first, since it's `None` when the user set no budget.
|
|
22
|
+
|
|
23
|
+
Everything runs INLINE and synchronously inside the eval call — no background mode, no resume, no separate progress app. Each eval call is one well-scoped fan-out; chain several across calls and turns for multi-phase work, reading each result before you decide the next phase.
|
|
24
|
+
</helpers>
|
|
48
25
|
|
|
49
26
|
<structure>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
{
|
|
70
|
-
|
|
71
|
-
|
|
27
|
+
For independent per-item chains (review → verify, fetch → extract → score), wrap the WHOLE chain in one function and run it with `parallel()` — then each item flows through its own steps without waiting on the others:
|
|
28
|
+
|
|
29
|
+
**Python (`eval`, Python backend):**
|
|
30
|
+
|
|
31
|
+
DIMENSIONS = [{"key": "bugs", "prompt": "…"}, {"key": "perf", "prompt": "…"}]
|
|
32
|
+
def review_and_verify(d):
|
|
33
|
+
found = agent(d["prompt"], label=f"review:{d['key']}", schema=FINDINGS_SCHEMA)
|
|
34
|
+
return parallel([lambda f=f: {**f, "verdict": agent(
|
|
35
|
+
f"Refute if you can (default refuted when unsure): {f['title']}",
|
|
36
|
+
label=f"verify:{f['file']}", schema=VERDICT_SCHEMA)} for f in found["findings"]])
|
|
37
|
+
phase("Review")
|
|
38
|
+
results = parallel([lambda d=d: review_and_verify(d) for d in DIMENSIONS])
|
|
39
|
+
confirmed = [f for group in results for f in group if f["verdict"]["is_real"]]
|
|
40
|
+
|
|
41
|
+
**JavaScript (`eval`, JavaScript backend):**
|
|
42
|
+
|
|
43
|
+
const DIMENSIONS = [{ key: "bugs", prompt: "…" }, { key: "perf", prompt: "…" }];
|
|
44
|
+
async function reviewAndVerify(d) {
|
|
45
|
+
const found = await agent(d.prompt, {
|
|
46
|
+
label: `review:${d.key}`,
|
|
47
|
+
schema: FINDINGS_SCHEMA,
|
|
48
|
+
});
|
|
49
|
+
return await parallel(found.findings.map((f) => async () => ({
|
|
50
|
+
…f,
|
|
51
|
+
verdict: await agent(
|
|
52
|
+
`Refute if you can (default refuted when unsure): ${f.title}`,
|
|
53
|
+
{ label: `verify:${f.file}`, schema: VERDICT_SCHEMA },
|
|
54
|
+
),
|
|
55
|
+
})));
|
|
56
|
+
}
|
|
57
|
+
phase("Review");
|
|
58
|
+
const results = await parallel(DIMENSIONS.map((d) => async () => reviewAndVerify(d)));
|
|
59
|
+
const confirmed = results.flat().filter((f) => f.verdict.is_real);
|
|
60
|
+
Reach for `pipeline()` only when a stage genuinely needs ALL of the previous stage first — dedup/merge across the whole set, early-exit on zero, or "compare against the other findings" — because its inter-stage barrier makes every item wait for the slowest peer:
|
|
61
|
+
|
|
62
|
+
**Python (`eval`, Python backend):**
|
|
63
|
+
|
|
64
|
+
phase("Find")
|
|
65
|
+
found = parallel([lambda d=d: agent(d["prompt"], schema=FINDINGS_SCHEMA) for d in DIMENSIONS])
|
|
66
|
+
findings = dedupe([f for r in found for f in r["findings"]]) # needs everything at once
|
|
67
|
+
phase("Verify")
|
|
68
|
+
verdicts = parallel([lambda f=f: agent(verify_prompt(f), schema=VERDICT_SCHEMA) for f in findings])
|
|
69
|
+
|
|
70
|
+
**JavaScript (`eval`, JavaScript backend):**
|
|
71
|
+
|
|
72
|
+
phase("Find");
|
|
73
|
+
const found = await parallel(DIMENSIONS.map((d) => async () =>
|
|
74
|
+
await agent(d.prompt, { schema: FINDINGS_SCHEMA }),
|
|
75
|
+
));
|
|
76
|
+
const findings = dedupe(found.flatMap((r) => r.findings)); // needs everything at once
|
|
77
|
+
phase("Verify");
|
|
78
|
+
const verdicts = await parallel(findings.map((f) => async () =>
|
|
79
|
+
await agent(verifyPrompt(f), { schema: VERDICT_SCHEMA }),
|
|
80
|
+
));
|
|
81
|
+
Use ordinary code between calls to flatten/map/filter; don't add a barrier just for that. Nested `parallel()` pools each cap independently, so keep total fan-out sane.
|
|
72
82
|
</structure>
|
|
73
83
|
|
|
74
84
|
<patterns>
|
|
75
|
-
|
|
76
|
-
- **
|
|
77
|
-
- **
|
|
78
|
-
- **
|
|
79
|
-
- **
|
|
85
|
+
Compose the harness the task calls for:
|
|
86
|
+
- **Adversarial verify** — N independent skeptics per finding, each prompted to REFUTE; keep it only if a majority survive. `votes = parallel([lambda i=i: agent(f"Refute: {claim}. refuted=true if unsure.", schema=VERDICT) for i in range(3)])`, then keep when `sum(not v["refuted"] for v in votes) ≥ 2`.
|
|
87
|
+
- **Perspective-diverse verify** — give each verifier a distinct lens (correctness, security, perf, does-it-reproduce) instead of N identical refuters.
|
|
88
|
+
- **Judge panel** — N attempts from different angles, scored by parallel judges; synthesize from the winner, graft the best of the rest.
|
|
89
|
+
- **Loop-until-dry** — for unknown-size discovery, keep spawning finders until K consecutive rounds surface nothing new; dedup against everything SEEN, not just what was confirmed, or it never converges.
|
|
90
|
+
- **Multi-modal sweep** — parallel finders each searching a different way (by-container, by-content, by-entity, by-time), each blind to the others.
|
|
91
|
+
- **Completeness critic** — a final agent that asks "what's missing — modality not run, claim unverified, file unread?"; its answer is the next round.
|
|
92
|
+
- **Budget/count loops** — Python: `while len(bugs) < 10:`; JavaScript: `while (bugs.length < 10) { … }`. In Python, gate an explicit budget with `budget.total` and `budget.remaining()`; in JavaScript, use `await budget.total()` and `await budget.remaining()`. `log()` each round.
|
|
93
|
+
- **No silent caps** — if you bound coverage (top-N, no-retry, sampling), `log()` what you dropped; silent truncation reads as "covered everything" when it didn't.
|
|
94
|
+
|
|
95
|
+
Scale to the ask: "find any bugs" → a few finders, single-vote verify. "thoroughly audit / be comprehensive" → larger finder pool, 3–5-vote adversarial pass, a synthesis stage.
|
|
80
96
|
</patterns>
|
|
81
97
|
|
|
82
98
|
<execution>
|
|
83
|
-
-
|
|
84
|
-
|
|
85
|
-
-
|
|
86
|
-
-
|
|
87
|
-
- Keep going until the task is closed — returned fan-out is a step, not a stopping point.
|
|
99
|
+
- Decompose the surface first; capture it in `todo` when it spans phases.
|
|
100
|
+
- Prefer `schema=` for any agent whose output you branch on.
|
|
101
|
+
- After a fan-out returns, YOU own correctness: read the artifacts, run the gate, verify before acting. Subagents do the legwork; they don't get the last word.
|
|
102
|
+
- Keep going until the task is closed — a returned fan-out is a step, not a stopping point.
|
|
88
103
|
</execution>
|
|
89
104
|
</system-notice>
|