@tt-a1i/openpi 0.1.0 → 0.1.1

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.
@@ -38,6 +38,7 @@ import {
38
38
  getMarkdownTheme,
39
39
  keyHint,
40
40
  type ExtensionAPI,
41
+ type SessionManager,
41
42
  type ExtensionContext,
42
43
  } from "@earendil-works/pi-coding-agent";
43
44
  import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
@@ -62,6 +63,17 @@ import {
62
63
  loadJournal,
63
64
  persistWorkflowJson,
64
65
  } from "./artifacts.ts";
66
+ import { createWorkflowHandoffRegistry } from "./handoff.ts";
67
+ import {
68
+ classifyInterruptedInvocation,
69
+ createInvocationIdentity,
70
+ requestInvocation,
71
+ transitionInvocation,
72
+ } from "./invocation-ledger.ts";
73
+ import {
74
+ normalizeWorkflowOperatorKey,
75
+ WorkflowOperatorRegistry,
76
+ } from "./operator.ts";
65
77
  import {
66
78
  agentCallKey,
67
79
  createReplayCache,
@@ -71,6 +83,7 @@ import {
71
83
  import { RunController } from "./controller.ts";
72
84
  import {
73
85
  normalizePersistedWorkflowDetails,
86
+ recoverStaleWorkflowDetails,
74
87
  sessionWorkflowRunIds,
75
88
  showWorkflowDashboard,
76
89
  } from "./dashboard.ts";
@@ -98,6 +111,7 @@ import {
98
111
  statusColor,
99
112
  statusWord,
100
113
  createUsageReader,
114
+ refreshWorkflowGraph,
101
115
  SQUARE,
102
116
  type AgentRecord,
103
117
  type WorkflowDetails,
@@ -166,6 +180,8 @@ interface ScriptAgentResult {
166
180
  ok: boolean;
167
181
  output: string;
168
182
  structured?: unknown;
183
+ /** Opaque same-run handle for bounded downstream handoff. */
184
+ ref?: string;
169
185
  acceptance?: AgentRecord["acceptance"];
170
186
  error?: string;
171
187
  }
@@ -180,6 +196,10 @@ interface AgentCallOptions {
180
196
  provider?: unknown;
181
197
  effort?: unknown;
182
198
  isolation?: unknown;
199
+ /** Reuse one in-memory child Session within this workflow run. */
200
+ operator?: unknown;
201
+ /** Same-run result refs to hydrate as bounded untrusted input data. */
202
+ inputs?: unknown;
183
203
  }
184
204
 
185
205
  const WorkflowParams = Type.Object({
@@ -745,6 +765,8 @@ export default function workflows(pi: ExtensionAPI) {
745
765
  workflowConfig.concurrency,
746
766
  workflowConfig.maxAgentCalls,
747
767
  );
768
+ const handoffs = createWorkflowHandoffRegistry();
769
+ const operators = new WorkflowOperatorRegistry();
748
770
 
749
771
  // Each concurrent child gets its own extension runtime. Children use the
750
772
  // parent's live trust decision; an isolated child gets its own cwd (its
@@ -779,6 +801,7 @@ export default function workflows(pi: ExtensionAPI) {
779
801
  };
780
802
  const emit = (checkpoint = true) => {
781
803
  if (runSettled) return;
804
+ refreshWorkflowGraph(details);
782
805
  if (checkpoint) persistence.checkpoint();
783
806
  if (emitTimer) return;
784
807
  emitTimer = setTimeout(
@@ -809,6 +832,16 @@ export default function workflows(pi: ExtensionAPI) {
809
832
  );
810
833
  for (const record of details.agents) {
811
834
  if (record.state !== "running") continue;
835
+ if (
836
+ record.invocation &&
837
+ record.invocation.executionState !== "settled" &&
838
+ record.invocation.executionState !== "uncertain"
839
+ ) {
840
+ record.invocation = classifyInterruptedInvocation(
841
+ record.invocation,
842
+ Date.now(),
843
+ );
844
+ }
812
845
  record.state = "error";
813
846
  record.error =
814
847
  record.error ?? "Agent did not settle before run cleanup";
@@ -816,6 +849,7 @@ export default function workflows(pi: ExtensionAPI) {
816
849
  }
817
850
  details.status = status;
818
851
  details.finishedAt = Date.now();
852
+ refreshWorkflowGraph(details);
819
853
  if (error) details.error = sanitizeWorkflowDisplayLine(error);
820
854
  return true;
821
855
  };
@@ -874,8 +908,15 @@ export default function workflows(pi: ExtensionAPI) {
874
908
  error: "Workflow was aborted before this agent started",
875
909
  };
876
910
  }
911
+ const startedAt = Date.now();
912
+ const callId = `${details.runId}:call:${index}`;
877
913
  const record: AgentRecord = {
878
914
  index,
915
+ callId,
916
+ invocation: requestInvocation(
917
+ createInvocationIdentity(details.runId, index),
918
+ startedAt,
919
+ ),
879
920
  label,
880
921
  phase:
881
922
  typeof opts.phase === "string"
@@ -884,20 +925,49 @@ export default function workflows(pi: ExtensionAPI) {
884
925
  state: "running",
885
926
  model: ctx.model?.id,
886
927
  contextWindow: ctx.model?.contextWindow,
887
- startedAt: Date.now(),
928
+ startedAt,
888
929
  preview: "",
889
930
  usage: emptyUsage(),
890
931
  transcript: [],
891
932
  };
892
933
  details.agents.push(record);
934
+ refreshWorkflowGraph(details);
893
935
  persistence.checkpoint({ immediate: true });
894
936
  emit(false);
895
937
 
896
938
  const fail = (error: string): ScriptAgentResult => {
897
939
  if (record.state === "running" && !runSettled) {
940
+ const at = Date.now();
941
+ if (
942
+ record.invocation?.admissionState === "pending" &&
943
+ record.invocation.executionState === "pending"
944
+ ) {
945
+ record.invocation = transitionInvocation(record.invocation, {
946
+ status: "rejected",
947
+ at,
948
+ });
949
+ } else if (
950
+ record.invocation?.admissionState === "claimed" &&
951
+ record.invocation.executionState === "running"
952
+ ) {
953
+ record.invocation = transitionInvocation(record.invocation, {
954
+ status: "settled",
955
+ outcome: "error",
956
+ at,
957
+ });
958
+ } else if (
959
+ record.invocation &&
960
+ record.invocation.executionState !== "settled" &&
961
+ record.invocation.executionState !== "uncertain"
962
+ ) {
963
+ record.invocation = classifyInterruptedInvocation(
964
+ record.invocation,
965
+ at,
966
+ );
967
+ }
898
968
  record.state = "error";
899
969
  record.error = sanitizeWorkflowDisplayLine(error);
900
- record.finishedAt = Date.now();
970
+ record.finishedAt = at;
901
971
  emit();
902
972
  }
903
973
  return { ok: false, output: "", error };
@@ -909,6 +979,45 @@ export default function workflows(pi: ExtensionAPI) {
909
979
  : String(promptValue ?? "");
910
980
  if (!basePrompt.trim())
911
981
  return fail("agent() requires a non-empty prompt string");
982
+
983
+ let operatorKey: string | undefined;
984
+ try {
985
+ if (opts.operator !== undefined) {
986
+ operatorKey = normalizeWorkflowOperatorKey(String(opts.operator));
987
+ if (opts.isolation !== undefined) {
988
+ throw new Error(
989
+ "workflow operators cannot use per-call worktree isolation",
990
+ );
991
+ }
992
+ record.operatorKey = operatorKey;
993
+ }
994
+ } catch (error) {
995
+ return fail(`agent "${label}": ${errorText(error)}`);
996
+ }
997
+
998
+ let inputRefs: string[] = [];
999
+ let promptWithHandoffs = basePrompt;
1000
+ try {
1001
+ if (opts.inputs !== undefined) {
1002
+ if (
1003
+ !Array.isArray(opts.inputs) ||
1004
+ !opts.inputs.every((value) => typeof value === "string")
1005
+ ) {
1006
+ throw new Error(
1007
+ "inputs must be an array of workflow result refs",
1008
+ );
1009
+ }
1010
+ inputRefs = [...opts.inputs];
1011
+ }
1012
+ const entries = handoffs.resolveEntries(inputRefs);
1013
+ record.inputCallIds = entries.flatMap((entry) =>
1014
+ entry.callId ? [entry.callId] : [],
1015
+ );
1016
+ promptWithHandoffs = handoffs.appendToPrompt(basePrompt, inputRefs);
1017
+ } catch (error) {
1018
+ return fail(`agent "${label}": ${errorText(error)}`);
1019
+ }
1020
+
912
1021
  let acceptanceContract: ReturnType<typeof parseAcceptanceContract>;
913
1022
  let effectiveSchema: unknown;
914
1023
  try {
@@ -921,8 +1030,8 @@ export default function workflows(pi: ExtensionAPI) {
921
1030
  }
922
1031
  const prompt = buildWorkflowAgentPrompt(
923
1032
  acceptanceContract
924
- ? `${basePrompt}\n\n${acceptanceInstruction(acceptanceContract)}`
925
- : basePrompt,
1033
+ ? `${promptWithHandoffs}\n\n${acceptanceInstruction(acceptanceContract)}`
1034
+ : promptWithHandoffs,
926
1035
  );
927
1036
  if (controller.signal.aborted)
928
1037
  return fail("Workflow was aborted before this agent started");
@@ -1012,16 +1121,36 @@ export default function workflows(pi: ExtensionAPI) {
1012
1121
  }
1013
1122
  record.model = model?.id;
1014
1123
  record.contextWindow = model?.contextWindow;
1124
+ const operatorFingerprint = operatorKey
1125
+ ? agentCallKey("workflow-operator", {
1126
+ execution: {
1127
+ agentType: agentType
1128
+ ? {
1129
+ name: agentType.name,
1130
+ body: agentType.body,
1131
+ tools: agentType.tools,
1132
+ }
1133
+ : undefined,
1134
+ model: model ? `${model.provider}/${model.id}` : undefined,
1135
+ effort: thinkingLevel,
1136
+ structured: effectiveSchema !== undefined,
1137
+ },
1138
+ })
1139
+ : undefined;
1015
1140
 
1016
1141
  // Replay is deliberately narrower than execution: only a named type
1017
1142
  // whose effective tool allowlist is entirely known read-only can be
1018
1143
  // cached. General-purpose children inherit bash/edit/write, custom
1019
1144
  // tools have unknown effects, and worktrees have state a string result
1020
1145
  // cannot restore.
1021
- const replaySafe = isReplaySafeAgentCall({
1022
- tools: agentType?.tools,
1023
- isolation: opts.isolation,
1024
- });
1146
+ // A reused operator's later activation depends on prior in-memory
1147
+ // conversation state, which a cached string cannot reconstruct.
1148
+ const replaySafe =
1149
+ operatorKey === undefined &&
1150
+ isReplaySafeAgentCall({
1151
+ tools: agentType?.tools,
1152
+ isolation: opts.isolation,
1153
+ });
1025
1154
  const replayLease = beginProcessReplayWorkspaceLease(replaySafe);
1026
1155
  let replayResources:
1027
1156
  Awaited<ReturnType<typeof getResources>> | undefined;
@@ -1070,9 +1199,14 @@ export default function workflows(pi: ExtensionAPI) {
1070
1199
  const cached =
1071
1200
  callKey && replayLease.canReplay ? replay?.take(callKey) : undefined;
1072
1201
  if (cached) {
1202
+ const finishedAt = Date.now();
1203
+ record.invocation = transitionInvocation(record.invocation!, {
1204
+ status: "replayed",
1205
+ at: finishedAt,
1206
+ });
1073
1207
  record.state = "done";
1074
1208
  record.replayed = true;
1075
- record.finishedAt = Date.now();
1209
+ record.finishedAt = finishedAt;
1076
1210
  record.preview = sanitizeWorkflowDisplayText(
1077
1211
  cached.output,
1078
1212
  PREVIEW_LENGTH,
@@ -1083,6 +1217,16 @@ export default function workflows(pi: ExtensionAPI) {
1083
1217
  cached.structured,
1084
1218
  );
1085
1219
  }
1220
+ const ref = handoffs.register({
1221
+ callId,
1222
+ settled: true,
1223
+ ok: true,
1224
+ output: cached.output,
1225
+ ...(cached.structured !== undefined
1226
+ ? { structured: cached.structured }
1227
+ : {}),
1228
+ });
1229
+ if (ref) record.resultRef = ref;
1086
1230
  emit();
1087
1231
  // Re-journal so a chain of resumes keeps working: run C resuming from
1088
1232
  // B still finds what B replayed from A.
@@ -1094,12 +1238,24 @@ export default function workflows(pi: ExtensionAPI) {
1094
1238
  ...(cached.structured !== undefined
1095
1239
  ? { structured: cached.structured }
1096
1240
  : {}),
1241
+ ...(ref ? { ref } : {}),
1097
1242
  ...(record.acceptance ? { acceptance: record.acceptance } : {}),
1098
1243
  };
1099
1244
  }
1100
1245
 
1101
1246
  return controller
1102
1247
  .schedule(async (runSignal) => {
1248
+ const claimedAt = Date.now();
1249
+ record.invocation = transitionInvocation(record.invocation!, {
1250
+ status: "claimed",
1251
+ at: claimedAt,
1252
+ });
1253
+ refreshWorkflowGraph(details);
1254
+ persistence.checkpoint({ immediate: true });
1255
+ record.invocation = transitionInvocation(record.invocation, {
1256
+ status: "running",
1257
+ at: Date.now(),
1258
+ });
1103
1259
  record.model = model?.id;
1104
1260
  record.contextWindow = model?.contextWindow;
1105
1261
  emit();
@@ -1172,45 +1328,58 @@ export default function workflows(pi: ExtensionAPI) {
1172
1328
  if (runSettled) {
1173
1329
  throw new Error("Workflow was settled before agent creation");
1174
1330
  }
1175
- const outcome = await runAgent({
1176
- prompt,
1177
- schema: effectiveSchema,
1178
- model,
1179
- thinkingLevel,
1180
- // Replay-safe calls use the same canonical cwd as the
1181
- // identity and filesystem boundary, so a symlink spelling of
1182
- // the checkout cannot retarget relative tool paths.
1183
- cwd: replayIdentity?.cwd ?? agentCwd,
1184
- loader: resources.loader,
1185
- settingsManager: resources.settingsManager,
1186
- modelRegistry: ctx.modelRegistry,
1187
- ...(agentType?.tools ? { tools: agentType.tools } : {}),
1188
- ...(replayIdentity
1189
- ? {
1190
- replayFilesystemBoundary: {
1191
- repositoryRoot: replayIdentity.repositoryRoot,
1192
- cwd: replayIdentity.cwd,
1193
- onViolation: () => {
1194
- replayBoundaryViolated = true;
1331
+ const runChild = (sessionManager?: SessionManager) =>
1332
+ runAgent({
1333
+ prompt,
1334
+ schema: effectiveSchema,
1335
+ model,
1336
+ thinkingLevel,
1337
+ // Replay-safe calls use the same canonical cwd as the
1338
+ // identity and filesystem boundary, so a symlink spelling of
1339
+ // the checkout cannot retarget relative tool paths.
1340
+ cwd: replayIdentity?.cwd ?? agentCwd,
1341
+ loader: resources.loader,
1342
+ settingsManager: resources.settingsManager,
1343
+ ...(sessionManager ? { sessionManager } : {}),
1344
+ modelRegistry: ctx.modelRegistry,
1345
+ ...(agentType?.tools ? { tools: agentType.tools } : {}),
1346
+ ...(replayIdentity
1347
+ ? {
1348
+ replayFilesystemBoundary: {
1349
+ repositoryRoot: replayIdentity.repositoryRoot,
1350
+ cwd: replayIdentity.cwd,
1351
+ onViolation: () => {
1352
+ replayBoundaryViolated = true;
1353
+ },
1195
1354
  },
1196
- },
1197
- }
1198
- : {}),
1199
- signal: runSignal,
1200
- onProgress: (progress) => {
1201
- if (runSettled || record.state !== "running") return;
1202
- record.preview = sanitizeWorkflowDisplayText(
1203
- progress.preview,
1204
- PREVIEW_LENGTH,
1205
- );
1206
- record.usage = progress.usage;
1207
- record.model = progress.model ?? record.model;
1208
- record.contextWindow =
1209
- progress.contextWindow ?? record.contextWindow;
1210
- record.transcript = progress.transcript;
1211
- emit();
1212
- },
1213
- });
1355
+ }
1356
+ : {}),
1357
+ signal: runSignal,
1358
+ onProgress: (progress) => {
1359
+ if (runSettled || record.state !== "running") return;
1360
+ record.preview = sanitizeWorkflowDisplayText(
1361
+ progress.preview,
1362
+ PREVIEW_LENGTH,
1363
+ );
1364
+ record.usage = progress.usage;
1365
+ record.model = progress.model ?? record.model;
1366
+ record.contextWindow =
1367
+ progress.contextWindow ?? record.contextWindow;
1368
+ record.transcript = progress.transcript;
1369
+ emit();
1370
+ },
1371
+ });
1372
+ const outcome = operatorKey
1373
+ ? await operators.activate(
1374
+ {
1375
+ key: operatorKey,
1376
+ fingerprint: operatorFingerprint!,
1377
+ cwd: agentCwd,
1378
+ signal: runSignal,
1379
+ },
1380
+ runChild,
1381
+ )
1382
+ : await runChild();
1214
1383
 
1215
1384
  if (runSettled || record.state !== "running") {
1216
1385
  return {
@@ -1228,7 +1397,8 @@ export default function workflows(pi: ExtensionAPI) {
1228
1397
  outcome.output || record.preview,
1229
1398
  PREVIEW_LENGTH,
1230
1399
  );
1231
- record.finishedAt = Date.now();
1400
+ const finishedAt = Date.now();
1401
+ record.finishedAt = finishedAt;
1232
1402
  const judged = applyAcceptance({
1233
1403
  contract: acceptanceContract,
1234
1404
  structured: outcome.structured,
@@ -1238,12 +1408,27 @@ export default function workflows(pi: ExtensionAPI) {
1238
1408
  const acceptance = judged.ledger;
1239
1409
  if (acceptance) record.acceptance = acceptance;
1240
1410
  const outcomeOk = judged.ok;
1411
+ record.invocation = transitionInvocation(record.invocation!, {
1412
+ status: "settled",
1413
+ outcome: outcomeOk ? "success" : "error",
1414
+ at: finishedAt,
1415
+ });
1241
1416
  record.state = outcomeOk ? "done" : "error";
1242
1417
  if (outcomeOk) delete record.error;
1243
1418
  else
1244
1419
  record.error = judged.error
1245
1420
  ? sanitizeWorkflowDisplayLine(judged.error)
1246
1421
  : undefined;
1422
+ const ref = handoffs.register({
1423
+ callId,
1424
+ settled: true,
1425
+ ok: outcomeOk,
1426
+ output: outcome.output,
1427
+ ...(outcome.structured !== undefined
1428
+ ? { structured: outcome.structured }
1429
+ : {}),
1430
+ });
1431
+ if (ref) record.resultRef = ref;
1247
1432
  emit();
1248
1433
 
1249
1434
  // Only provably read-only successes with a complete, stable
@@ -1285,6 +1470,7 @@ export default function workflows(pi: ExtensionAPI) {
1285
1470
  ...(outcome.structured !== undefined
1286
1471
  ? { structured: outcome.structured }
1287
1472
  : {}),
1473
+ ...(ref ? { ref } : {}),
1288
1474
  ...(acceptance ? { acceptance } : {}),
1289
1475
  ...(record.error !== undefined ? { error: record.error } : {}),
1290
1476
  };
@@ -1379,12 +1565,16 @@ export default function workflows(pi: ExtensionAPI) {
1379
1565
  const settled = await controller.settle({
1380
1566
  abort: status !== "completed",
1381
1567
  });
1568
+ const operatorsSettled = await waitBounded(operators.close(), 1_000);
1382
1569
  if (runSettled) return;
1383
- if (!settled) {
1570
+ if (!settled || !operatorsSettled) {
1384
1571
  status = "failed";
1572
+ const cleanupError = !settled
1573
+ ? "agent shutdown deadline exceeded"
1574
+ : "workflow operator cleanup deadline exceeded";
1385
1575
  details.error = details.error
1386
- ? `${details.error}; agent shutdown deadline exceeded`
1387
- : "Agent shutdown deadline exceeded";
1576
+ ? `${details.error}; ${cleanupError}`
1577
+ : cleanupError[0]!.toUpperCase() + cleanupError.slice(1);
1388
1578
  }
1389
1579
  if (runSettled) return;
1390
1580
  terminalize(status, details.error);
@@ -1744,10 +1934,7 @@ export default function workflows(pi: ExtensionAPI) {
1744
1934
  // shutdown settle deadline.
1745
1935
  return {
1746
1936
  ok: true,
1747
- details:
1748
- details.status === "running"
1749
- ? { ...details, status: "aborted" as const }
1750
- : details,
1937
+ details: recoverStaleWorkflowDetails(details),
1751
1938
  } as const;
1752
1939
  } catch {
1753
1940
  return {