@tt-a1i/openpi 0.5.0 → 0.6.0
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 +18 -10
- package/SETUP.md +8 -2
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/bin/openpi.js +25 -15
- package/extensions/ai-providers/LICENSE.upstream +23 -0
- package/extensions/ai-providers/README.md +59 -0
- package/extensions/ai-providers/antigravity/credentials.ts +52 -0
- package/extensions/ai-providers/antigravity/discovery.ts +130 -0
- package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
- package/extensions/ai-providers/antigravity/models.ts +84 -0
- package/extensions/ai-providers/antigravity/oauth.ts +700 -0
- package/extensions/ai-providers/antigravity/provider.ts +1116 -0
- package/extensions/ai-providers/antigravity/routing.ts +340 -0
- package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
- package/extensions/ai-providers/cursor/constants.ts +5 -0
- package/extensions/ai-providers/cursor/credentials.ts +14 -0
- package/extensions/ai-providers/cursor/discovery.ts +291 -0
- package/extensions/ai-providers/cursor/input-images.ts +106 -0
- package/extensions/ai-providers/cursor/models.ts +45 -0
- package/extensions/ai-providers/cursor/oauth.ts +263 -0
- package/extensions/ai-providers/cursor/proto.ts +1064 -0
- package/extensions/ai-providers/cursor/protobuf.ts +1171 -0
- package/extensions/ai-providers/cursor/provider.ts +1175 -0
- package/extensions/ai-providers/cursor/proxy.ts +213 -0
- package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
- package/extensions/ai-providers/index.ts +86 -0
- package/extensions/ai-providers/oauth-adapter.ts +81 -0
- package/extensions/ai-providers/usage.ts +10 -0
- package/extensions/background-terminals/index.ts +8 -1
- package/extensions/background-terminals/src/manager.ts +3 -5
- package/extensions/background-terminals/src/result-delivery.ts +43 -23
- package/extensions/cron/index.ts +68 -27
- package/extensions/cron/schedule.ts +5 -1
- package/extensions/model-info/cache-diagnostics.ts +220 -0
- package/extensions/model-info/index.ts +45 -1
- package/extensions/plan-mode/index.ts +75 -4
- package/extensions/setup/index.ts +15 -3
- package/extensions/shared/child-session.ts +25 -5
- package/extensions/shared/completion-inbox.ts +193 -0
- package/extensions/shared/setup-config.ts +10 -1
- package/extensions/shared/structured-output.ts +154 -0
- package/extensions/subagents/index.ts +44 -4
- package/extensions/subagents/src/backends/pi.ts +76 -5
- package/extensions/subagents/src/domain.ts +16 -1
- package/extensions/subagents/src/manager.ts +5 -0
- package/extensions/subagents/src/prompt.ts +17 -3
- package/extensions/subagents/src/result-artifact.ts +32 -0
- package/extensions/subagents/src/result-delivery.ts +33 -14
- package/extensions/ui-customization/footer.ts +16 -5
- package/extensions/user-input-fold/index.ts +42 -6
- package/extensions/web/index.ts +25 -2
- package/extensions/workflows/acceptance.ts +43 -19
- package/extensions/workflows/completion-projection.ts +3 -1
- package/extensions/workflows/dashboard.ts +8 -0
- package/extensions/workflows/index.ts +13 -0
- package/extensions/workflows/model.ts +5 -1
- package/extensions/workflows/prompt.ts +4 -10
- package/extensions/workflows/result-delivery.ts +96 -22
- package/extensions/workflows/retention.ts +6 -0
- package/extensions/workflows/runner.ts +6 -71
- package/package.json +7 -7
- package/skills/subagents/REFERENCE.md +3 -2
- package/skills/subagents/SKILL.md +1 -0
- package/skills/workflows/REFERENCE.md +3 -3
- package/skills/workflows/SKILL.md +1 -1
- package/web/adapter/pi-adapter.ts +3 -0
- package/web/host/pi-coding-agent-entry.ts +162 -0
- package/web/host/web-host.ts +330 -50
- package/web/protocol/types.ts +5 -0
- package/web/runtime/pi-runtime.ts +240 -25
- package/web/runtime/types.ts +32 -1
- package/web/ui/app.js +343 -41
- package/web/ui/index.html +3 -0
- package/web/ui/styles.css +119 -37
|
@@ -67,6 +67,10 @@ export type WorkflowDeliveryState =
|
|
|
67
67
|
export interface WorkflowDelivery {
|
|
68
68
|
/** Stable per-run idempotency identity, never a transport-batch id. */
|
|
69
69
|
id: string;
|
|
70
|
+
/** Destination transcript identity; legacy records fall back to run.sessionId. */
|
|
71
|
+
ownerSessionId?: string;
|
|
72
|
+
/** Process-local generation of the Pi SessionManager owner. */
|
|
73
|
+
ownerEpoch?: number;
|
|
70
74
|
state: WorkflowDeliveryState;
|
|
71
75
|
attempts: number;
|
|
72
76
|
updatedAt: number;
|
|
@@ -123,7 +127,7 @@ export interface AgentRecord {
|
|
|
123
127
|
usage: AgentUsage;
|
|
124
128
|
/** Replayed from a prior run's journal instead of actually executed. */
|
|
125
129
|
replayed?: boolean;
|
|
126
|
-
/**
|
|
130
|
+
/** Deprecated child self-attestation; never runtime-observed evidence. */
|
|
127
131
|
acceptance?: AcceptanceLedger;
|
|
128
132
|
/** Branch of the isolated worktree this agent ran in, when it holds commits. */
|
|
129
133
|
worktreeBranch?: string;
|
|
@@ -56,7 +56,7 @@ export const WORKFLOW_TOOL_DESCRIPTION = [
|
|
|
56
56
|
"Interactive sessions launch in the background by default and deliver completion later. Set wait: true only when this tool call must return the final result inline.",
|
|
57
57
|
"Derive fan-out from independent verifiable work items and task difficulty. Concurrency is a runtime ceiling, not a target or the total-call limit; user cost, count, model, and effort constraints take precedence.",
|
|
58
58
|
"For concurrent writers use isolation: 'worktree' and tell each agent to commit. Read-only work should normally stay in the shared checkout.",
|
|
59
|
-
"Read the workflows Skill before a nontrivial script; it covers the restricted sandbox, full DSL,
|
|
59
|
+
"Read the workflows Skill before a nontrivial script; it covers the restricted sandbox, full DSL, result refs, replay, background lifecycle, limits, and examples.",
|
|
60
60
|
].join("\n");
|
|
61
61
|
|
|
62
62
|
/** Adds workflow orchestration primitives and background execution to the model's tool prompt. */
|
|
@@ -75,14 +75,6 @@ export function buildWorkflowAgentPrompt(prompt: string) {
|
|
|
75
75
|
return prompt;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
/** Instructs structured workflow children to terminate with exactly one structured_output call. */
|
|
79
|
-
export const STRUCTURED_OUTPUT_SYSTEM_INSTRUCTION =
|
|
80
|
-
"When your task is complete, call the `structured_output` tool exactly once as your final action, with fields matching the required schema. Do not write any other text after it.";
|
|
81
|
-
|
|
82
|
-
/** Describes the terminating structured_output tool and its final-action contract. */
|
|
83
|
-
export const STRUCTURED_OUTPUT_TOOL_DESCRIPTION =
|
|
84
|
-
"Return your final result as structured data matching the required schema. Call this exactly once, as your last action; do not write any other text after it.";
|
|
85
|
-
|
|
86
78
|
/** Builds the workflow completion report returned to the parent model. */
|
|
87
79
|
export function buildWorkflowResultMessage(
|
|
88
80
|
details: WorkflowDetails,
|
|
@@ -157,7 +149,9 @@ export function buildWorkflowResultMessage(
|
|
|
157
149
|
: "running";
|
|
158
150
|
lines.push(
|
|
159
151
|
`- [${agent.label}]${agent.phase ? ` (${agent.phase})` : ""} ${status}` +
|
|
160
|
-
(agent.acceptance
|
|
152
|
+
(agent.acceptance
|
|
153
|
+
? ` · deprecated model self-attestation ${agent.acceptance.status}`
|
|
154
|
+
: "") +
|
|
161
155
|
(agent.error ? ` — ${agent.error}` : ""),
|
|
162
156
|
);
|
|
163
157
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
type CompletionEnvelope,
|
|
3
|
+
type CompletionOwner,
|
|
4
|
+
createCompletionInbox,
|
|
5
|
+
} from "../shared/completion-inbox.ts";
|
|
2
6
|
import type {
|
|
3
7
|
DurableResultDeliveryQueue,
|
|
4
8
|
DurableResultDeliveryReceipt,
|
|
5
9
|
} from "../shared/result-delivery.ts";
|
|
10
|
+
import type { WorkflowDetails } from "./model.ts";
|
|
6
11
|
|
|
7
12
|
export interface WorkflowCompletionEnvelope {
|
|
8
13
|
deliveryId: string;
|
|
@@ -12,6 +17,7 @@ export interface WorkflowCompletionEnvelope {
|
|
|
12
17
|
|
|
13
18
|
export interface WorkflowResultDeliveryOptions {
|
|
14
19
|
isIdle: () => boolean;
|
|
20
|
+
owner?: () => CompletionOwner | undefined;
|
|
15
21
|
persist: (details: WorkflowDetails) => void;
|
|
16
22
|
deliver: (
|
|
17
23
|
envelopes: readonly WorkflowCompletionEnvelope[],
|
|
@@ -34,7 +40,9 @@ function errorText(error: unknown) {
|
|
|
34
40
|
export function createWorkflowResultDelivery(
|
|
35
41
|
options: WorkflowResultDeliveryOptions,
|
|
36
42
|
) {
|
|
37
|
-
const
|
|
43
|
+
const inbox = createCompletionInbox<WorkflowCompletionEnvelope>();
|
|
44
|
+
const currentOwner =
|
|
45
|
+
options.owner ?? (() => ({ sessionId: "test", epoch: 0 }));
|
|
38
46
|
let flushing: Promise<void> | undefined;
|
|
39
47
|
let flushRequested = false;
|
|
40
48
|
let wakeRequested = false;
|
|
@@ -57,10 +65,44 @@ export function createWorkflowResultDelivery(
|
|
|
57
65
|
options.persist(details);
|
|
58
66
|
};
|
|
59
67
|
|
|
60
|
-
const
|
|
61
|
-
|
|
68
|
+
const inboxEnvelope = (
|
|
69
|
+
envelope: WorkflowCompletionEnvelope,
|
|
70
|
+
): CompletionEnvelope<WorkflowCompletionEnvelope> => {
|
|
71
|
+
const delivery = envelope.details.delivery;
|
|
72
|
+
const deliverySessionId = delivery?.ownerSessionId;
|
|
73
|
+
const detailsSessionId = envelope.details.sessionId;
|
|
74
|
+
|
|
75
|
+
// Conflicting owner fields invalidate delivery ownership fail-closed.
|
|
76
|
+
const hasConflict =
|
|
77
|
+
deliverySessionId !== undefined &&
|
|
78
|
+
detailsSessionId !== undefined &&
|
|
79
|
+
deliverySessionId !== detailsSessionId;
|
|
80
|
+
|
|
81
|
+
const ownerSessionId = hasConflict ? undefined : deliverySessionId;
|
|
82
|
+
return {
|
|
83
|
+
deliveryId: envelope.deliveryId,
|
|
84
|
+
owner: {
|
|
85
|
+
sessionId:
|
|
86
|
+
ownerSessionId && ownerSessionId !== "unowned"
|
|
87
|
+
? ownerSessionId
|
|
88
|
+
: "unowned",
|
|
89
|
+
epoch: delivery?.ownerEpoch ?? 0,
|
|
90
|
+
},
|
|
91
|
+
producer: "workflow",
|
|
92
|
+
producerId: envelope.runId,
|
|
93
|
+
terminalRef: {
|
|
94
|
+
kind: "workflow-terminal",
|
|
95
|
+
runId: envelope.runId,
|
|
96
|
+
status: envelope.details.status,
|
|
97
|
+
},
|
|
98
|
+
wake: "producer-policy",
|
|
99
|
+
payload: envelope,
|
|
100
|
+
};
|
|
62
101
|
};
|
|
63
102
|
|
|
103
|
+
const enqueue = (envelope: WorkflowCompletionEnvelope) =>
|
|
104
|
+
inbox.defer(inboxEnvelope(envelope), currentOwner());
|
|
105
|
+
|
|
64
106
|
const retainPending = (
|
|
65
107
|
envelope: WorkflowCompletionEnvelope,
|
|
66
108
|
patch: Partial<NonNullable<WorkflowDetails["delivery"]>>,
|
|
@@ -69,7 +111,7 @@ export function createWorkflowResultDelivery(
|
|
|
69
111
|
// Memory owns the retry before persistence is attempted. A broken disk
|
|
70
112
|
// must not make this envelope, or any sibling after it, disappear from the
|
|
71
113
|
// current process.
|
|
72
|
-
enqueue(envelope);
|
|
114
|
+
const admitted = enqueue(envelope);
|
|
73
115
|
try {
|
|
74
116
|
persistState(envelope.details, "pending", patch);
|
|
75
117
|
} catch (error) {
|
|
@@ -83,6 +125,7 @@ export function createWorkflowResultDelivery(
|
|
|
83
125
|
};
|
|
84
126
|
}
|
|
85
127
|
}
|
|
128
|
+
return admitted;
|
|
86
129
|
};
|
|
87
130
|
|
|
88
131
|
const flush = async (wake: boolean) => {
|
|
@@ -91,17 +134,16 @@ export function createWorkflowResultDelivery(
|
|
|
91
134
|
wakeRequested ||= wake;
|
|
92
135
|
return flushing;
|
|
93
136
|
}
|
|
94
|
-
if (
|
|
137
|
+
if (inbox.size() === 0) return;
|
|
95
138
|
|
|
96
139
|
flushing = (async () => {
|
|
97
140
|
let passWake = wake;
|
|
98
|
-
while (
|
|
141
|
+
while (inbox.size() > 0) {
|
|
99
142
|
flushRequested = false;
|
|
100
143
|
wakeRequested = false;
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
144
|
+
const claimed = inbox.claim(currentOwner());
|
|
145
|
+
const envelopes = claimed.map((envelope) => envelope.payload);
|
|
146
|
+
if (envelopes.length === 0) break;
|
|
105
147
|
|
|
106
148
|
let receipts: readonly DurableResultDeliveryReceipt[] | undefined;
|
|
107
149
|
try {
|
|
@@ -109,6 +151,7 @@ export function createWorkflowResultDelivery(
|
|
|
109
151
|
} catch (error) {
|
|
110
152
|
const message = errorText(error);
|
|
111
153
|
for (const envelope of envelopes) {
|
|
154
|
+
inbox.acknowledge([envelope.deliveryId]);
|
|
112
155
|
retainPending(
|
|
113
156
|
envelope,
|
|
114
157
|
{
|
|
@@ -127,6 +170,7 @@ export function createWorkflowResultDelivery(
|
|
|
127
170
|
for (const envelope of envelopes) {
|
|
128
171
|
const receipt = byId.get(envelope.deliveryId);
|
|
129
172
|
if (receipt?.delivered) {
|
|
173
|
+
inbox.acknowledge([envelope.deliveryId]);
|
|
130
174
|
try {
|
|
131
175
|
persistState(envelope.details, "delivered", {
|
|
132
176
|
attempts: (envelope.details.delivery?.attempts ?? 0) + 1,
|
|
@@ -150,6 +194,7 @@ export function createWorkflowResultDelivery(
|
|
|
150
194
|
}
|
|
151
195
|
continue;
|
|
152
196
|
}
|
|
197
|
+
inbox.acknowledge([envelope.deliveryId]);
|
|
153
198
|
retainPending(
|
|
154
199
|
envelope,
|
|
155
200
|
{
|
|
@@ -183,7 +228,7 @@ export function createWorkflowResultDelivery(
|
|
|
183
228
|
|
|
184
229
|
/** Terminal won the wait/abort arbitration and will be returned inline. */
|
|
185
230
|
consumeInline(details: WorkflowDetails) {
|
|
186
|
-
if (details.delivery)
|
|
231
|
+
if (details.delivery) inbox.consumeDeliveryIds([details.delivery.id]);
|
|
187
232
|
persistState(details, "consumed-inline", {
|
|
188
233
|
deliveredAt: Date.now(),
|
|
189
234
|
lastError: undefined,
|
|
@@ -214,18 +259,46 @@ export function createWorkflowResultDelivery(
|
|
|
214
259
|
restore(envelope: WorkflowCompletionEnvelope) {
|
|
215
260
|
const state = envelope.details.delivery?.state;
|
|
216
261
|
if (state !== "pending" && state !== "held-for-inline") return false;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
262
|
+
const owner = currentOwner();
|
|
263
|
+
const deliverySessionId = envelope.details.delivery?.ownerSessionId;
|
|
264
|
+
const detailsSessionId = envelope.details.sessionId;
|
|
265
|
+
const hasConflict =
|
|
266
|
+
deliverySessionId !== undefined &&
|
|
267
|
+
detailsSessionId !== undefined &&
|
|
268
|
+
deliverySessionId !== detailsSessionId;
|
|
269
|
+
const storedSessionId = hasConflict
|
|
270
|
+
? undefined
|
|
271
|
+
: (deliverySessionId ?? detailsSessionId);
|
|
272
|
+
|
|
273
|
+
// Restoring canonical producer state is the explicit owner-revival
|
|
274
|
+
// boundary. Rebind only the same transcript to this process-local
|
|
275
|
+
// SessionManager generation; a different Session still dead-letters.
|
|
276
|
+
if (
|
|
277
|
+
owner &&
|
|
278
|
+
storedSessionId !== undefined &&
|
|
279
|
+
storedSessionId === owner.sessionId
|
|
280
|
+
) {
|
|
281
|
+
envelope.details.delivery = {
|
|
282
|
+
...envelope.details.delivery!,
|
|
283
|
+
ownerSessionId: owner.sessionId,
|
|
284
|
+
ownerEpoch: owner.epoch,
|
|
285
|
+
};
|
|
286
|
+
return retainPending(
|
|
221
287
|
envelope,
|
|
222
|
-
{
|
|
288
|
+
{
|
|
289
|
+
ownerSessionId: owner.sessionId,
|
|
290
|
+
ownerEpoch: owner.epoch,
|
|
291
|
+
lastError:
|
|
292
|
+
state === "held-for-inline"
|
|
293
|
+
? "Inline waiter was not active after session restart"
|
|
294
|
+
: undefined,
|
|
295
|
+
},
|
|
223
296
|
"Restored delivery state persistence failed",
|
|
224
297
|
);
|
|
225
|
-
} else {
|
|
226
|
-
enqueue(envelope);
|
|
227
298
|
}
|
|
228
|
-
|
|
299
|
+
// Keep the canonical terminal artifact pending, but record that this
|
|
300
|
+
// process is not its transcript owner instead of redirecting it.
|
|
301
|
+
return enqueue(envelope);
|
|
229
302
|
},
|
|
230
303
|
|
|
231
304
|
retryPending() {
|
|
@@ -242,12 +315,13 @@ export function createWorkflowResultDelivery(
|
|
|
242
315
|
},
|
|
243
316
|
|
|
244
317
|
size() {
|
|
245
|
-
return
|
|
318
|
+
return inbox.size();
|
|
246
319
|
},
|
|
247
320
|
|
|
248
321
|
clear() {
|
|
249
|
-
|
|
322
|
+
inbox.clear();
|
|
250
323
|
},
|
|
324
|
+
inspectDeadLetters: inbox.inspectDeadLetters,
|
|
251
325
|
};
|
|
252
326
|
return queue satisfies DurableResultDeliveryQueue<WorkflowCompletionEnvelope>;
|
|
253
327
|
}
|
|
@@ -237,6 +237,12 @@ function makeProjection(
|
|
|
237
237
|
? {
|
|
238
238
|
delivery: {
|
|
239
239
|
id: details.delivery.id,
|
|
240
|
+
...(details.delivery.ownerSessionId
|
|
241
|
+
? { ownerSessionId: details.delivery.ownerSessionId }
|
|
242
|
+
: {}),
|
|
243
|
+
...(details.delivery.ownerEpoch !== undefined
|
|
244
|
+
? { ownerEpoch: details.delivery.ownerEpoch }
|
|
245
|
+
: {}),
|
|
240
246
|
state: details.delivery.state,
|
|
241
247
|
attempts: details.delivery.attempts,
|
|
242
248
|
updatedAt: details.delivery.updatedAt,
|
|
@@ -16,14 +16,12 @@ import {
|
|
|
16
16
|
type AgentSessionEventListener,
|
|
17
17
|
createAgentSession,
|
|
18
18
|
DefaultResourceLoader,
|
|
19
|
-
defineTool,
|
|
20
19
|
type ExtensionAPI,
|
|
21
20
|
type ExtensionContext,
|
|
22
21
|
SessionManager,
|
|
23
22
|
SettingsManager,
|
|
24
23
|
type ToolDefinition,
|
|
25
24
|
} from "@earendil-works/pi-coding-agent";
|
|
26
|
-
import { type TSchema, Type } from "typebox";
|
|
27
25
|
import { AgentToolRenderLedger } from "../shared/agent-tool-renderer.ts";
|
|
28
26
|
import {
|
|
29
27
|
bindChildSessionExtensions,
|
|
@@ -34,10 +32,11 @@ import {
|
|
|
34
32
|
import { createToolCallTimeoutGuard } from "../shared/tool-call-timeout.ts";
|
|
35
33
|
import { type AgentUsage, emptyUsage, type TranscriptEntry } from "./model.ts";
|
|
36
34
|
import {
|
|
37
|
-
|
|
35
|
+
childToolsWithStructuredOutput,
|
|
36
|
+
createStructuredOutputTool,
|
|
38
37
|
STRUCTURED_OUTPUT_SYSTEM_INSTRUCTION,
|
|
39
|
-
|
|
40
|
-
} from "./prompt.ts";
|
|
38
|
+
} from "../shared/structured-output.ts";
|
|
39
|
+
import { buildWorkflowAgentPrompt } from "./prompt.ts";
|
|
41
40
|
import {
|
|
42
41
|
AgentProgressProjection,
|
|
43
42
|
type ProgressAssistantMessage,
|
|
@@ -142,9 +141,7 @@ export function workflowChildTools(
|
|
|
142
141
|
tools: readonly string[] | undefined,
|
|
143
142
|
structured: boolean,
|
|
144
143
|
) {
|
|
145
|
-
return tools
|
|
146
|
-
? [...new Set([...tools, ...(structured ? ["structured_output"] : [])])]
|
|
147
|
-
: undefined;
|
|
144
|
+
return childToolsWithStructuredOutput(tools, structured);
|
|
148
145
|
}
|
|
149
146
|
|
|
150
147
|
interface WorkflowToolSession {
|
|
@@ -179,68 +176,6 @@ export function guardWorkflowChildTools(
|
|
|
179
176
|
});
|
|
180
177
|
}
|
|
181
178
|
|
|
182
|
-
function isJsonSchema(value: unknown): value is TSchema {
|
|
183
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
184
|
-
const seen = new WeakSet<object>();
|
|
185
|
-
let nodes = 0;
|
|
186
|
-
const validate = (current: unknown, depth: number): boolean => {
|
|
187
|
-
if (++nodes > 10_000 || depth > 24) return false;
|
|
188
|
-
if (
|
|
189
|
-
current === null ||
|
|
190
|
-
typeof current === "string" ||
|
|
191
|
-
typeof current === "boolean"
|
|
192
|
-
) {
|
|
193
|
-
return true;
|
|
194
|
-
}
|
|
195
|
-
if (typeof current === "number") return Number.isFinite(current);
|
|
196
|
-
if (Array.isArray(current)) {
|
|
197
|
-
return current.every((item) => validate(item, depth + 1));
|
|
198
|
-
}
|
|
199
|
-
if (typeof current !== "object") return false;
|
|
200
|
-
if (seen.has(current)) return false;
|
|
201
|
-
seen.add(current);
|
|
202
|
-
return Object.keys(current).every((key) => {
|
|
203
|
-
if (key === "__proto__" || key === "constructor" || key === "prototype") {
|
|
204
|
-
return false;
|
|
205
|
-
}
|
|
206
|
-
return validate((current as Record<string, unknown>)[key], depth + 1);
|
|
207
|
-
});
|
|
208
|
-
};
|
|
209
|
-
return validate(value, 0);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/** Preserve the caller's full JSON Schema instead of lossy keyword conversion. */
|
|
213
|
-
function jsonSchemaToTypebox(schema: unknown): TSchema {
|
|
214
|
-
if (!isJsonSchema(schema)) {
|
|
215
|
-
throw new Error("structured output schema must be a bounded JSON object");
|
|
216
|
-
}
|
|
217
|
-
return Type.Unsafe(schema);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* One-shot terminating tool injected when a schema is supplied: the subagent
|
|
222
|
-
* calls it as its final action and we capture the validated object.
|
|
223
|
-
*/
|
|
224
|
-
function makeStructuredOutputTool(
|
|
225
|
-
schema: unknown,
|
|
226
|
-
capture: (value: unknown) => void,
|
|
227
|
-
): ToolDefinition {
|
|
228
|
-
return defineTool({
|
|
229
|
-
name: "structured_output",
|
|
230
|
-
label: "Structured Output",
|
|
231
|
-
description: STRUCTURED_OUTPUT_TOOL_DESCRIPTION,
|
|
232
|
-
parameters: jsonSchemaToTypebox(schema),
|
|
233
|
-
async execute(_toolCallId, params) {
|
|
234
|
-
capture(params);
|
|
235
|
-
return {
|
|
236
|
-
content: [{ type: "text", text: "Recorded structured result." }],
|
|
237
|
-
details: params,
|
|
238
|
-
terminate: true,
|
|
239
|
-
};
|
|
240
|
-
},
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
|
|
244
179
|
type AssistantMessage = ProgressAssistantMessage;
|
|
245
180
|
|
|
246
181
|
export { transcriptFromMessages };
|
|
@@ -475,7 +410,7 @@ export async function runAgent(
|
|
|
475
410
|
customTools =
|
|
476
411
|
options.schema !== undefined
|
|
477
412
|
? [
|
|
478
|
-
|
|
413
|
+
createStructuredOutputTool(options.schema, (value) => {
|
|
479
414
|
if (!settled) structured = value;
|
|
480
415
|
}),
|
|
481
416
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tt-a1i/openpi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "OpenPI — a Pi-native multi-agent workbench with background execution, isolated subagents, replay-safe workflows, goals, tasks, and observable TUI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "tt-a1i",
|
|
@@ -63,9 +63,9 @@
|
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@biomejs/biome": "2.5.8",
|
|
66
|
-
"@earendil-works/pi-ai": "^0.
|
|
67
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
68
|
-
"@earendil-works/pi-tui": "^0.
|
|
66
|
+
"@earendil-works/pi-ai": "^0.85.1",
|
|
67
|
+
"@earendil-works/pi-coding-agent": "^0.85.1",
|
|
68
|
+
"@earendil-works/pi-tui": "^0.85.1",
|
|
69
69
|
"@effect/tsgo": "^0.24.2",
|
|
70
70
|
"@effect/vitest": "^4.0.0-beta.99",
|
|
71
71
|
"@types/node": "^26.1.1",
|
|
@@ -75,9 +75,9 @@
|
|
|
75
75
|
"vitest": "4.1.10"
|
|
76
76
|
},
|
|
77
77
|
"peerDependencies": {
|
|
78
|
-
"@earendil-works/pi-ai": "
|
|
79
|
-
"@earendil-works/pi-coding-agent": "
|
|
80
|
-
"@earendil-works/pi-tui": "
|
|
78
|
+
"@earendil-works/pi-ai": ">=0.85.1",
|
|
79
|
+
"@earendil-works/pi-coding-agent": ">=0.85.1",
|
|
80
|
+
"@earendil-works/pi-tui": ">=0.85.1",
|
|
81
81
|
"typebox": "*"
|
|
82
82
|
},
|
|
83
83
|
"type": "module",
|
|
@@ -153,8 +153,9 @@ So `tools: [read, grep, find, ls]` yields a child that genuinely has no
|
|
|
153
153
|
`write`, `edit`, or `bash` tool to call — not one that has been asked not to.
|
|
154
154
|
Parent-only names are removed before the generated roster and spawn result are
|
|
155
155
|
shown, so a type that lists `subagent_spawn` never advertises it as usable.
|
|
156
|
-
A
|
|
157
|
-
`
|
|
156
|
+
A Workflow child with a schema, or a Direct Subagent spawned with
|
|
157
|
+
`output_schema`, additionally receives only its terminating `structured_output`
|
|
158
|
+
tool; this does not restore any denied repository tool.
|
|
158
159
|
|
|
159
160
|
While `/plan` is armed, `isolation: "worktree"` is rejected before Git is
|
|
160
161
|
changed. A selected type whose declared tools plan mode would narrow (such as
|
|
@@ -13,6 +13,7 @@ The tool definitions are canonical for parameters, limits, model syntax, isolati
|
|
|
13
13
|
- Prefer a matching agent type when one exists; its tool restriction is enforced. Model precedence is explicit spawn override, selected type-file model, configured built-in role model, then parent model. Reasoning precedence is explicit spawn override, selected type default, then parent effort. Types live in `~/.pi/agent/agents/*.md` and, for trusted projects, `.pi/agents/*.md`; see [Agent types](REFERENCE.md).
|
|
14
14
|
- Isolate concurrent writers in worktrees according to the `subagent_spawn` schema so they cannot overwrite one checkout or git index. While Plan Mode is active, use only read-only exploration types (or no type); worktree isolation and types narrowed by Plan Mode are rejected.
|
|
15
15
|
- After spawning, continue useful parent work. In an interactive session, if none remains, tell the user the child is still running and end the turn; automatic result delivery will re-invoke the parent when it settles. Do not block merely because the next step depends on the result or because there is nothing else to do. Use `subagent_wait` only when the user explicitly asks to keep the current response open for the result, or when non-interactive automation must return it in the same invocation.
|
|
16
|
+
- Use optional `output_schema` when downstream work needs a machine-validated result rather than prose. The child then receives one terminating `structured_output` tool, and the run fails if it finishes without submitting a matching value. Keep schemas small and task-specific; the validated JSON is delivered to the parent and preserved in a private content-addressed artifact. Omit the option for ordinary text reports.
|
|
16
17
|
|
|
17
18
|
## Worktree isolation
|
|
18
19
|
|
|
@@ -12,13 +12,13 @@ The `workflow` script is an async JavaScript function body executed in a restric
|
|
|
12
12
|
|
|
13
13
|
## Agent calls
|
|
14
14
|
|
|
15
|
-
`await agent(prompt, options)` runs one child and always resolves to `{ ok, output, structured?, ref?, acceptance?, error? }`. Check `ok` before reading output. Children receive normal trust-aware resources but cannot recursively orchestrate or ask the user.
|
|
15
|
+
`await agent(prompt, options)` runs one child and always resolves to `{ ok, output, structured?, ref?, acceptance?, acceptanceWarning?, error? }`. Check `ok` before reading output. Children receive normal trust-aware resources but cannot recursively orchestrate or ask the user.
|
|
16
16
|
|
|
17
|
-
Useful options include `agent_type`, `label`, `phase`, `schema`, `
|
|
17
|
+
Useful options include `agent_type`, `label`, `phase`, `schema`, `model`, `provider`, `effort`, `isolation`, `operator`, and `inputs`. The legacy `acceptance` option remains readable only during the 0.x migration window described below.
|
|
18
18
|
|
|
19
19
|
- Prefer a matching `agent_type`. Model precedence is explicit model/provider, type file, configured built-in role, then parent. Effort precedence is explicit effort, type default, then parent.
|
|
20
20
|
- `schema` validates structured output. Use it whenever later workflow logic branches on fields.
|
|
21
|
-
- `acceptance
|
|
21
|
+
- `acceptance` is deprecated since OpenPI 0.5 and scheduled for removal in 1.0. Compatibility calls still return the child-authored ledger with `authority: "model-self-attestation"` and a migration warning, but it never determines `ok`. Use ordinary `schema` for findings, then let the parent evaluate them alongside runtime-observed exit codes, test receipts, file fingerprints, and tool results. Old DSL, journals, and artifacts remain readable during 0.x.
|
|
22
22
|
- `operator: "name"` reuses one in-memory child Session for serialized follow-ups inside the same run. Its model, role/tools, effort, structured mode, and cwd are frozen by the first activation. Operators cannot use per-call worktrees or replay, and do not survive restarts.
|
|
23
23
|
- `inputs: [ref, ...]` accepts successful opaque refs from the same workflow run only. Each conclusion is bounded to 16 KiB and total injected input to 48 KiB. The total budget is fairly distributed, so a large fan-out cannot starve later results merely because of order; partial projections are labeled. Full successful child results remain in the run's `agent-results/` artifacts. Inputs are marked as untrusted data; the resulting graph is observability, not scheduling authority.
|
|
24
24
|
- Fair projection preserves the head and tail of every partial result and names its run-relative `agent-results/agent-N.json` audit artifact. That path is provenance for the parent/operator, not a child-readable handle. Fair presence is not proof of full evidence coverage: for large fan-out, group source refs into local Report agents, then pass only their refs to a global Report. The workflow script—not Runtime—must state planned, selected, covered, failed, and deferred counts.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: workflows
|
|
3
|
-
description: Orchestrates multi-agent work with OpenPI's inline JavaScript Workflow DSL. Use when a task needs multi-phase fan-out, pipelines, barriers, structured handoffs,
|
|
3
|
+
description: Orchestrates multi-agent work with OpenPI's inline JavaScript Workflow DSL. Use when a task needs multi-phase fan-out, pipelines, barriers, structured handoffs, or resumable background orchestration.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Workflows
|
|
@@ -490,6 +490,9 @@ export class PiWebAdapter {
|
|
|
490
490
|
status: this.runtime.isIdle()
|
|
491
491
|
? ("idle" as const)
|
|
492
492
|
: ("running" as const),
|
|
493
|
+
...(this.runtime.getActiveTurn()
|
|
494
|
+
? { activeTurn: this.runtime.getActiveTurn() }
|
|
495
|
+
: {}),
|
|
493
496
|
capabilities: webCapabilitySnapshot(this.runtime.sessionManager),
|
|
494
497
|
},
|
|
495
498
|
truncation: {
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
export const PI_CODING_AGENT_PACKAGE = "@earendil-works/pi-coding-agent";
|
|
6
|
+
export const PI_CODING_AGENT_ENTRY_ENV = "OPENPI_PI_CODING_AGENT_ENTRY";
|
|
7
|
+
const PACKAGE_ROOT_SEARCH_DEPTH = 10;
|
|
8
|
+
|
|
9
|
+
type PackageManifest = {
|
|
10
|
+
name?: unknown;
|
|
11
|
+
main?: unknown;
|
|
12
|
+
exports?: Record<string, { import?: unknown } | string>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function findPackageRoot(realPath: string, packageName: string) {
|
|
16
|
+
let dir = dirname(realPath);
|
|
17
|
+
for (let depth = 0; depth < PACKAGE_ROOT_SEARCH_DEPTH; depth++) {
|
|
18
|
+
const manifestPath = join(dir, "package.json");
|
|
19
|
+
if (existsSync(manifestPath)) {
|
|
20
|
+
const manifest = readManifest(manifestPath);
|
|
21
|
+
if (manifest?.name === packageName) return dir;
|
|
22
|
+
}
|
|
23
|
+
const parent = dirname(dir);
|
|
24
|
+
if (parent === dir) break;
|
|
25
|
+
dir = parent;
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readManifest(manifestPath: string) {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(readFileSync(manifestPath, "utf8")) as PackageManifest;
|
|
33
|
+
} catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function officialEntry(root: string | undefined) {
|
|
39
|
+
if (!root) return undefined;
|
|
40
|
+
const manifest = readManifest(join(root, "package.json"));
|
|
41
|
+
const target = manifest?.exports?.["."];
|
|
42
|
+
const relative =
|
|
43
|
+
typeof target === "string"
|
|
44
|
+
? target
|
|
45
|
+
: typeof target?.import === "string"
|
|
46
|
+
? target.import
|
|
47
|
+
: typeof manifest?.main === "string"
|
|
48
|
+
? manifest.main
|
|
49
|
+
: "dist/index.js";
|
|
50
|
+
const entry = join(root, relative);
|
|
51
|
+
try {
|
|
52
|
+
return existsSync(entry) && statSync(entry).isFile()
|
|
53
|
+
? realpathSync(entry)
|
|
54
|
+
: undefined;
|
|
55
|
+
} catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function walkFromFile(file: string) {
|
|
61
|
+
try {
|
|
62
|
+
const real = realpathSync(file);
|
|
63
|
+
if (!statSync(real).isFile()) return undefined;
|
|
64
|
+
return officialEntry(findPackageRoot(real, PI_CODING_AGENT_PACKAGE));
|
|
65
|
+
} catch {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function fileFromUrl(fromUrl: string) {
|
|
71
|
+
return fromUrl.startsWith("file:") ? fileURLToPath(fromUrl) : fromUrl;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function nearestPackageRoot(file: string) {
|
|
75
|
+
let dir = dirname(file);
|
|
76
|
+
for (let depth = 0; depth < PACKAGE_ROOT_SEARCH_DEPTH; depth++) {
|
|
77
|
+
if (existsSync(join(dir, "package.json"))) return dir;
|
|
78
|
+
const parent = dirname(dir);
|
|
79
|
+
if (parent === dir) break;
|
|
80
|
+
dir = parent;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function peerAt(nodeModules: string) {
|
|
85
|
+
const root = join(nodeModules, ...PI_CODING_AGENT_PACKAGE.split("/"));
|
|
86
|
+
const manifest = readManifest(join(root, "package.json"));
|
|
87
|
+
return manifest?.name === PI_CODING_AGENT_PACKAGE
|
|
88
|
+
? officialEntry(root)
|
|
89
|
+
: undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function resolveFromInstall(fromUrl: string) {
|
|
93
|
+
let start = fileFromUrl(fromUrl);
|
|
94
|
+
try {
|
|
95
|
+
start = realpathSync(start);
|
|
96
|
+
} catch {
|
|
97
|
+
// Keep the unresolved path when the caller file is a test stub.
|
|
98
|
+
}
|
|
99
|
+
const packageRoot = nearestPackageRoot(start);
|
|
100
|
+
if (!packageRoot) return undefined;
|
|
101
|
+
|
|
102
|
+
const nested = peerAt(join(packageRoot, "node_modules"));
|
|
103
|
+
if (nested) return nested;
|
|
104
|
+
|
|
105
|
+
const parent = dirname(packageRoot);
|
|
106
|
+
const grandparent = dirname(parent);
|
|
107
|
+
const hoistedModules =
|
|
108
|
+
basename(parent).startsWith("@") && basename(grandparent) === "node_modules"
|
|
109
|
+
? grandparent
|
|
110
|
+
: basename(parent) === "node_modules"
|
|
111
|
+
? parent
|
|
112
|
+
: undefined;
|
|
113
|
+
return hoistedModules ? peerAt(hoistedModules) : undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function validatePiCodingAgentEntry(candidate: string | undefined) {
|
|
117
|
+
if (!candidate) return undefined;
|
|
118
|
+
return walkFromFile(candidate);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function missingPiCodingAgentDiagnostic() {
|
|
122
|
+
return [
|
|
123
|
+
`OpenPI Web could not resolve ${PI_CODING_AGENT_PACKAGE} for this process.`,
|
|
124
|
+
"Host resolution uses only the current process argv identity and fail-closes if that path is not the official package.",
|
|
125
|
+
`${PI_CODING_AGENT_ENTRY_ENV} is an explicit standalone handoff, not a host fallback.`,
|
|
126
|
+
`Standalone openpi web uses that handoff when valid, then the installed nested or hoisted peer (npm install ${PI_CODING_AGENT_PACKAGE}).`,
|
|
127
|
+
"From a running Pi session use /web, which hands over the host Pi.",
|
|
128
|
+
"Supported package install is `pi install npm:@tt-a1i/openpi`.",
|
|
129
|
+
].join(" ");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function resolvePiCodingAgentEntry(options?: {
|
|
133
|
+
source?: "host" | "standalone";
|
|
134
|
+
env?: NodeJS.ProcessEnv;
|
|
135
|
+
argv1?: string | undefined;
|
|
136
|
+
fromUrl?: string;
|
|
137
|
+
}) {
|
|
138
|
+
const source = options?.source ?? "host";
|
|
139
|
+
if (source === "standalone") {
|
|
140
|
+
const env = options?.env ?? process.env;
|
|
141
|
+
const handed = validatePiCodingAgentEntry(env[PI_CODING_AGENT_ENTRY_ENV]);
|
|
142
|
+
if (handed) return handed;
|
|
143
|
+
return resolveFromInstall(options?.fromUrl ?? import.meta.url);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const argv1 = options?.argv1 === undefined ? process.argv[1] : options.argv1;
|
|
147
|
+
return argv1 ? walkFromFile(argv1) : undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function resolveStandaloneJitiAliases(options?: {
|
|
151
|
+
env?: NodeJS.ProcessEnv;
|
|
152
|
+
argv1?: string | undefined;
|
|
153
|
+
fromUrl?: string;
|
|
154
|
+
}) {
|
|
155
|
+
const fromUrl = options?.fromUrl ?? import.meta.url;
|
|
156
|
+
const entry = resolvePiCodingAgentEntry({
|
|
157
|
+
...options,
|
|
158
|
+
fromUrl,
|
|
159
|
+
source: "standalone",
|
|
160
|
+
});
|
|
161
|
+
return entry ? { [PI_CODING_AGENT_PACKAGE]: entry } : {};
|
|
162
|
+
}
|