@tt-a1i/openpi 0.6.0 → 0.6.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.
- package/README.md +13 -11
- package/SETUP.md +2 -2
- package/extensions/ai-providers/README.md +12 -6
- package/extensions/ai-providers/cursor/input-images.ts +2 -3
- package/extensions/ai-providers/cursor/proto.ts +218 -11
- package/extensions/ai-providers/cursor/protobuf.ts +12 -2
- package/extensions/ai-providers/cursor/provider.ts +276 -20
- package/extensions/ai-providers/cursor/tool-bridge.ts +68 -0
- package/extensions/ai-providers/index.ts +3 -3
- package/extensions/shared/child-session.ts +14 -0
- package/extensions/subagents/index.ts +20 -3
- package/extensions/subagents/src/agent-types.ts +5 -17
- package/extensions/subagents/src/backends/pi.ts +70 -59
- package/extensions/subagents/src/backends/tool-preview.ts +29 -0
- package/extensions/subagents/src/manager.ts +2 -71
- package/extensions/subagents/src/prompt.ts +2 -2
- package/extensions/subagents/src/runtime.ts +10 -3
- package/extensions/workflows/dashboard.ts +139 -21
- package/extensions/workflows/index.ts +62 -20
- package/extensions/workflows/progress-projection.ts +7 -1
- package/extensions/workflows/runner.ts +5 -162
- package/extensions/workflows/sandbox.ts +4 -0
- package/package.json +1 -1
- package/skills/subagents/REFERENCE.md +6 -7
- package/skills/subagents/SKILL.md +1 -1
- package/skills/workflows/REFERENCE.md +3 -1
|
@@ -143,18 +143,54 @@ export function readPersistedWorkflowDetails(
|
|
|
143
143
|
runId: string,
|
|
144
144
|
options: ReadPersistedRunOptions = {},
|
|
145
145
|
): WorkflowDetails | undefined {
|
|
146
|
-
|
|
146
|
+
const details = normalizeReadRecord(
|
|
147
|
+
runId,
|
|
148
|
+
readPersistedWorkflowRecord(runId),
|
|
149
|
+
);
|
|
150
|
+
if (!details) return undefined;
|
|
151
|
+
if (options.hydrateArtifacts) hydrateRunArtifacts(runId, details);
|
|
152
|
+
return details;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeReadRecord(runId: string, raw: unknown) {
|
|
156
|
+
try {
|
|
157
|
+
return normalizePersistedWorkflowDetails(runId, raw);
|
|
158
|
+
} catch {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function readPersistedWorkflowRecord(runId: string) {
|
|
147
164
|
try {
|
|
148
165
|
const raw: unknown = JSON.parse(
|
|
149
166
|
fs.readFileSync(path.join(runsDir(), runId, "workflow.json"), "utf8"),
|
|
150
167
|
);
|
|
151
|
-
|
|
168
|
+
return raw && typeof raw === "object"
|
|
169
|
+
? (raw as Record<string, unknown>)
|
|
170
|
+
: undefined;
|
|
152
171
|
} catch {
|
|
153
172
|
return undefined;
|
|
154
173
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function matchesRunScope(
|
|
177
|
+
record: { startedAt?: unknown; finishedAt?: unknown; sessionId?: unknown },
|
|
178
|
+
runId: string,
|
|
179
|
+
sessionId: string,
|
|
180
|
+
referencedRunIds: ReadonlySet<string>,
|
|
181
|
+
startedSince: number,
|
|
182
|
+
fromRetention = false,
|
|
183
|
+
) {
|
|
184
|
+
const touchedAt = Math.max(
|
|
185
|
+
typeof record.startedAt === "number" ? record.startedAt : 0,
|
|
186
|
+
typeof record.finishedAt === "number" ? record.finishedAt : 0,
|
|
187
|
+
);
|
|
188
|
+
return (
|
|
189
|
+
touchedAt >= startedSince &&
|
|
190
|
+
(fromRetention ||
|
|
191
|
+
record.sessionId === sessionId ||
|
|
192
|
+
referencedRunIds.has(runId))
|
|
193
|
+
);
|
|
158
194
|
}
|
|
159
195
|
|
|
160
196
|
function isWorktreeCleanup(
|
|
@@ -581,27 +617,41 @@ export function loadRunEntries(
|
|
|
581
617
|
retained: ReadonlyMap<string, WorkflowDetails> = new Map(),
|
|
582
618
|
): RunEntry[] {
|
|
583
619
|
const entries: RunEntry[] = [];
|
|
584
|
-
const runIds = new Set([
|
|
620
|
+
const runIds = new Set([
|
|
621
|
+
...listPersistedRunIds(),
|
|
622
|
+
...retained.keys(),
|
|
623
|
+
...active.keys(),
|
|
624
|
+
]);
|
|
585
625
|
for (const runId of runIds) {
|
|
586
626
|
const live = active.get(runId);
|
|
587
627
|
if (live) {
|
|
588
628
|
entries.push({ runId, details: live, live: true });
|
|
589
629
|
continue;
|
|
590
630
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
631
|
+
// Reject unrelated history before normalizing potentially large inline
|
|
632
|
+
// transcripts. Side artifacts belong to explicit detail navigation.
|
|
633
|
+
const raw = readPersistedWorkflowRecord(runId);
|
|
634
|
+
if (
|
|
635
|
+
raw &&
|
|
636
|
+
!matchesRunScope(raw, runId, sessionId, referencedRunIds, startedSince)
|
|
637
|
+
) {
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
const persisted = normalizeReadRecord(runId, raw);
|
|
594
641
|
const retainedDetails = retained.get(runId);
|
|
595
642
|
const details = persisted ?? retainedDetails;
|
|
596
643
|
if (!details) continue;
|
|
597
644
|
const fromRetention =
|
|
598
645
|
persisted === undefined && retainedDetails !== undefined;
|
|
599
|
-
const touchedAt = Math.max(details.startedAt, details.finishedAt ?? 0);
|
|
600
646
|
if (
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
647
|
+
!matchesRunScope(
|
|
648
|
+
details,
|
|
649
|
+
runId,
|
|
650
|
+
sessionId,
|
|
651
|
+
referencedRunIds,
|
|
652
|
+
startedSince,
|
|
653
|
+
fromRetention,
|
|
654
|
+
)
|
|
605
655
|
) {
|
|
606
656
|
continue;
|
|
607
657
|
}
|
|
@@ -718,6 +768,9 @@ type DetailFocus = "phases" | "agents";
|
|
|
718
768
|
export class WorkflowDashboard {
|
|
719
769
|
private view: View = "list";
|
|
720
770
|
private entries: RunEntry[] = [];
|
|
771
|
+
private historyLoaded = false;
|
|
772
|
+
private seenRetainedRunIds = new Set<string>();
|
|
773
|
+
private hydratedRunIds = new Set<string>();
|
|
721
774
|
private listIndex = 0;
|
|
722
775
|
private phaseIndex = 0;
|
|
723
776
|
private agentIndex = 0;
|
|
@@ -826,13 +879,68 @@ export class WorkflowDashboard {
|
|
|
826
879
|
|
|
827
880
|
private refresh() {
|
|
828
881
|
const selected = this.entries[this.listIndex]?.runId;
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
this.
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
882
|
+
const active = this.getActive();
|
|
883
|
+
const retained = this.getRetained();
|
|
884
|
+
if (!this.historyLoaded) {
|
|
885
|
+
this.entries = loadRunEntries(
|
|
886
|
+
active,
|
|
887
|
+
this.sessionId,
|
|
888
|
+
this.referencedRunIds,
|
|
889
|
+
this.startedSince,
|
|
890
|
+
retained,
|
|
891
|
+
);
|
|
892
|
+
this.historyLoaded = true;
|
|
893
|
+
} else {
|
|
894
|
+
// Animation ticks reuse historical projections. Only a newly settled run
|
|
895
|
+
// needs one canonical read; stable frames never scan or reread history.
|
|
896
|
+
const entries = new Map(
|
|
897
|
+
this.entries.map((entry) => [entry.runId, entry]),
|
|
898
|
+
);
|
|
899
|
+
const settledIds = new Set([
|
|
900
|
+
...this.entries
|
|
901
|
+
.filter((entry) => entry.live && !active.has(entry.runId))
|
|
902
|
+
.map((entry) => entry.runId),
|
|
903
|
+
...[...retained.keys()].filter(
|
|
904
|
+
(runId) =>
|
|
905
|
+
!this.seenRetainedRunIds.has(runId) &&
|
|
906
|
+
!entries.has(runId) &&
|
|
907
|
+
!active.has(runId),
|
|
908
|
+
),
|
|
909
|
+
]);
|
|
910
|
+
for (const runId of settledIds) {
|
|
911
|
+
const persisted = readPersistedWorkflowDetails(runId);
|
|
912
|
+
const details =
|
|
913
|
+
persisted ?? retained.get(runId) ?? entries.get(runId)?.details;
|
|
914
|
+
if (!details) continue;
|
|
915
|
+
if (
|
|
916
|
+
!matchesRunScope(
|
|
917
|
+
details,
|
|
918
|
+
runId,
|
|
919
|
+
this.sessionId,
|
|
920
|
+
this.referencedRunIds,
|
|
921
|
+
this.startedSince,
|
|
922
|
+
!persisted,
|
|
923
|
+
)
|
|
924
|
+
) {
|
|
925
|
+
entries.delete(runId);
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
// Recovery operates on a projection, never on the former live owner.
|
|
929
|
+
const recovered = recoverStaleWorkflowDetails({
|
|
930
|
+
...details,
|
|
931
|
+
agents: details.agents.map((agent) => ({ ...agent })),
|
|
932
|
+
});
|
|
933
|
+
entries.set(runId, { runId, details: recovered, live: false });
|
|
934
|
+
this.hydratedRunIds.delete(runId);
|
|
935
|
+
}
|
|
936
|
+
for (const [runId, details] of active) {
|
|
937
|
+
entries.set(runId, { runId, details, live: true });
|
|
938
|
+
}
|
|
939
|
+
this.entries = [...entries.values()].sort(
|
|
940
|
+
(a, b) => b.details.startedAt - a.details.startedAt,
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
for (const runId of retained.keys()) this.seenRetainedRunIds.add(runId);
|
|
836
944
|
if (selected) {
|
|
837
945
|
const index = this.entries.findIndex((e) => e.runId === selected);
|
|
838
946
|
if (index >= 0) this.listIndex = index;
|
|
@@ -847,6 +955,7 @@ export class WorkflowDashboard {
|
|
|
847
955
|
);
|
|
848
956
|
if (refreshed) this.current = refreshed;
|
|
849
957
|
}
|
|
958
|
+
if (this.view === "transcript") this.hydrateCurrent();
|
|
850
959
|
if (this.notice && Date.now() - this.noticeAt > NOTICE_TTL_MS)
|
|
851
960
|
this.notice = undefined;
|
|
852
961
|
}
|
|
@@ -882,7 +991,15 @@ export class WorkflowDashboard {
|
|
|
882
991
|
this.agentIndex = Math.min(this.agentIndex, Math.max(0, agents.length - 1));
|
|
883
992
|
}
|
|
884
993
|
|
|
994
|
+
private hydrateCurrent() {
|
|
995
|
+
const entry = this.current;
|
|
996
|
+
if (!entry || entry.live || this.hydratedRunIds.has(entry.runId)) return;
|
|
997
|
+
hydrateRunArtifacts(entry.runId, entry.details);
|
|
998
|
+
this.hydratedRunIds.add(entry.runId);
|
|
999
|
+
}
|
|
1000
|
+
|
|
885
1001
|
private saveReport() {
|
|
1002
|
+
this.hydrateCurrent();
|
|
886
1003
|
const entry = this.current;
|
|
887
1004
|
if (!entry) return;
|
|
888
1005
|
const target = path.join(runsDir(), entry.runId, "report.md");
|
|
@@ -1023,6 +1140,7 @@ export class WorkflowDashboard {
|
|
|
1023
1140
|
}
|
|
1024
1141
|
|
|
1025
1142
|
private openTranscriptPage() {
|
|
1143
|
+
this.hydrateCurrent();
|
|
1026
1144
|
const transcriptAdapter = new WorkflowTranscriptAdapter();
|
|
1027
1145
|
this.view = "transcript";
|
|
1028
1146
|
this.transcriptPage = new AgentSessionPage(
|
|
@@ -53,7 +53,11 @@ import {
|
|
|
53
53
|
formatActivityStatus,
|
|
54
54
|
} from "../shared/activity-status.ts";
|
|
55
55
|
import { fitNavigationSides } from "../shared/below-editor-navigation.ts";
|
|
56
|
-
import {
|
|
56
|
+
import {
|
|
57
|
+
inheritedChildToolAllowlist,
|
|
58
|
+
resolveStandaloneChildProjectTrust,
|
|
59
|
+
waitBounded,
|
|
60
|
+
} from "../shared/child-session.ts";
|
|
57
61
|
import { contextPercent } from "../shared/context-utilization.ts";
|
|
58
62
|
import { completionOwnerFor } from "../shared/completion-inbox.ts";
|
|
59
63
|
import {
|
|
@@ -544,6 +548,7 @@ interface ScriptAgentResult {
|
|
|
544
548
|
}
|
|
545
549
|
|
|
546
550
|
interface AgentCallOptions {
|
|
551
|
+
working_dir?: unknown;
|
|
547
552
|
agent_type?: unknown;
|
|
548
553
|
label?: unknown;
|
|
549
554
|
phase?: unknown;
|
|
@@ -1339,11 +1344,12 @@ export default function workflows(
|
|
|
1339
1344
|
structured: boolean,
|
|
1340
1345
|
cwd: string,
|
|
1341
1346
|
agentTypePrompt?: string,
|
|
1347
|
+
childProjectTrusted = projectTrusted,
|
|
1342
1348
|
) =>
|
|
1343
1349
|
createWorkflowResources(
|
|
1344
1350
|
cwd,
|
|
1345
1351
|
structured ? "structured" : "plain",
|
|
1346
|
-
|
|
1352
|
+
childProjectTrusted,
|
|
1347
1353
|
agentTypePrompt,
|
|
1348
1354
|
);
|
|
1349
1355
|
|
|
@@ -1638,6 +1644,36 @@ export default function workflows(
|
|
|
1638
1644
|
);
|
|
1639
1645
|
}
|
|
1640
1646
|
|
|
1647
|
+
const childTools = inheritedChildToolAllowlist(
|
|
1648
|
+
pi.getActiveTools(),
|
|
1649
|
+
agentType?.tools,
|
|
1650
|
+
);
|
|
1651
|
+
if (
|
|
1652
|
+
opts.working_dir !== undefined &&
|
|
1653
|
+
(typeof opts.working_dir !== "string" || !opts.working_dir.trim())
|
|
1654
|
+
) {
|
|
1655
|
+
return fail(
|
|
1656
|
+
`agent "${label}": working_dir must be a non-empty string`,
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1659
|
+
const requestedCwd = path.resolve(
|
|
1660
|
+
ctx.cwd,
|
|
1661
|
+
typeof opts.working_dir === "string" ? opts.working_dir : ".",
|
|
1662
|
+
);
|
|
1663
|
+
try {
|
|
1664
|
+
if (!fs.statSync(requestedCwd).isDirectory())
|
|
1665
|
+
throw new Error("not a directory");
|
|
1666
|
+
} catch {
|
|
1667
|
+
return fail(
|
|
1668
|
+
`agent "${label}": working_dir is not a directory: ${requestedCwd}`,
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1671
|
+
const childProjectTrusted = resolveStandaloneChildProjectTrust({
|
|
1672
|
+
parentCwd: ctx.cwd,
|
|
1673
|
+
childCwd: requestedCwd,
|
|
1674
|
+
parentTrusted: projectTrusted,
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1641
1677
|
const explicitModel =
|
|
1642
1678
|
typeof opts.model === "string" && opts.model.trim()
|
|
1643
1679
|
? opts.model.trim()
|
|
@@ -1708,11 +1744,14 @@ export default function workflows(
|
|
|
1708
1744
|
const operatorFingerprint = operatorKey
|
|
1709
1745
|
? agentCallKey("workflow-operator", {
|
|
1710
1746
|
execution: {
|
|
1747
|
+
cwd: requestedCwd,
|
|
1748
|
+
projectTrusted: childProjectTrusted,
|
|
1749
|
+
tools: childTools,
|
|
1711
1750
|
agentType: agentType
|
|
1712
1751
|
? {
|
|
1713
1752
|
name: agentType.name,
|
|
1714
1753
|
body: agentType.body,
|
|
1715
|
-
tools:
|
|
1754
|
+
tools: childTools,
|
|
1716
1755
|
}
|
|
1717
1756
|
: undefined,
|
|
1718
1757
|
model: model ? `${model.provider}/${model.id}` : undefined,
|
|
@@ -1732,7 +1771,7 @@ export default function workflows(
|
|
|
1732
1771
|
const replaySafe =
|
|
1733
1772
|
operatorKey === undefined &&
|
|
1734
1773
|
isReplaySafeAgentCall({
|
|
1735
|
-
tools: agentType?.tools,
|
|
1774
|
+
tools: agentType?.tools === undefined ? undefined : childTools,
|
|
1736
1775
|
isolation: opts.isolation,
|
|
1737
1776
|
});
|
|
1738
1777
|
const replayLease = beginProcessReplayWorkspaceLease(replaySafe);
|
|
@@ -1744,13 +1783,14 @@ export default function workflows(
|
|
|
1744
1783
|
try {
|
|
1745
1784
|
replayResources = await getResources(
|
|
1746
1785
|
effectiveSchema !== undefined,
|
|
1747
|
-
|
|
1786
|
+
requestedCwd,
|
|
1748
1787
|
agentType?.body,
|
|
1788
|
+
childProjectTrusted,
|
|
1749
1789
|
);
|
|
1750
1790
|
replayIdentity = createReplayIdentity(
|
|
1751
|
-
|
|
1791
|
+
requestedCwd,
|
|
1752
1792
|
replayResources.loader,
|
|
1753
|
-
|
|
1793
|
+
childProjectTrusted,
|
|
1754
1794
|
);
|
|
1755
1795
|
} catch {
|
|
1756
1796
|
// Fingerprinting is an optimization boundary. If resources cannot
|
|
@@ -1768,7 +1808,7 @@ export default function workflows(
|
|
|
1768
1808
|
? {
|
|
1769
1809
|
name: agentType.name,
|
|
1770
1810
|
body: agentType.body,
|
|
1771
|
-
tools:
|
|
1811
|
+
tools: childTools,
|
|
1772
1812
|
}
|
|
1773
1813
|
: undefined,
|
|
1774
1814
|
model: model ? `${model.provider}/${model.id}` : undefined,
|
|
@@ -1925,7 +1965,7 @@ export default function workflows(
|
|
|
1925
1965
|
);
|
|
1926
1966
|
}
|
|
1927
1967
|
const created = await createWorktree({
|
|
1928
|
-
cwd:
|
|
1968
|
+
cwd: requestedCwd,
|
|
1929
1969
|
label,
|
|
1930
1970
|
id: `${details.runId}-${record.index}`,
|
|
1931
1971
|
});
|
|
@@ -1937,18 +1977,19 @@ export default function workflows(
|
|
|
1937
1977
|
worktree = created.worktree;
|
|
1938
1978
|
if (!runSettled) record.worktreeBranch = worktree.branch;
|
|
1939
1979
|
}
|
|
1940
|
-
|
|
1941
|
-
throw runSignal.reason instanceof Error
|
|
1942
|
-
? runSignal.reason
|
|
1943
|
-
: new Error("Workflow was aborted");
|
|
1944
|
-
}
|
|
1945
|
-
const agentCwd = worktree?.path ?? ctx.cwd;
|
|
1980
|
+
const agentCwd = worktree?.path ?? requestedCwd;
|
|
1946
1981
|
|
|
1947
1982
|
// Inside the try, not before it: building resources can throw
|
|
1948
1983
|
// (bad settings, an unreadable skills dir), and a throw out here
|
|
1949
1984
|
// would skip the finally and leak the worktree permanently —
|
|
1950
1985
|
// nothing sweeps `.git/pi-worktrees/` afterwards.
|
|
1951
1986
|
try {
|
|
1987
|
+
if (runSignal.aborted || runSettled) {
|
|
1988
|
+
throw runSignal.reason instanceof Error
|
|
1989
|
+
? runSignal.reason
|
|
1990
|
+
: new Error("Workflow was aborted");
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1952
1993
|
let rejectResourceLoad: (() => void) | undefined;
|
|
1953
1994
|
const resourceAbort = new Promise<never>((_resolve, reject) => {
|
|
1954
1995
|
rejectResourceLoad = () =>
|
|
@@ -1968,6 +2009,7 @@ export default function workflows(
|
|
|
1968
2009
|
effectiveSchema !== undefined,
|
|
1969
2010
|
agentCwd,
|
|
1970
2011
|
agentType?.body,
|
|
2012
|
+
childProjectTrusted,
|
|
1971
2013
|
),
|
|
1972
2014
|
resourceAbort,
|
|
1973
2015
|
]).finally(() => {
|
|
@@ -1992,7 +2034,7 @@ export default function workflows(
|
|
|
1992
2034
|
settingsManager: resources.settingsManager,
|
|
1993
2035
|
...(sessionManager ? { sessionManager } : {}),
|
|
1994
2036
|
modelRegistry: ctx.modelRegistry,
|
|
1995
|
-
|
|
2037
|
+
tools: childTools,
|
|
1996
2038
|
...(testAgentSessionFactory
|
|
1997
2039
|
? { sessionFactory: testAgentSessionFactory }
|
|
1998
2040
|
: {}),
|
|
@@ -2108,9 +2150,9 @@ export default function workflows(
|
|
|
2108
2150
|
// unfingerprintable calls always run for real.
|
|
2109
2151
|
const completedIdentity = callKey
|
|
2110
2152
|
? createReplayIdentity(
|
|
2111
|
-
|
|
2153
|
+
requestedCwd,
|
|
2112
2154
|
resources.loader,
|
|
2113
|
-
|
|
2155
|
+
childProjectTrusted,
|
|
2114
2156
|
)
|
|
2115
2157
|
: undefined;
|
|
2116
2158
|
const completedKey = completedIdentity
|
|
@@ -2157,7 +2199,7 @@ export default function workflows(
|
|
|
2157
2199
|
runId: details.runId,
|
|
2158
2200
|
agentIndex: record.index,
|
|
2159
2201
|
agentLabel: record.label,
|
|
2160
|
-
repoCwd:
|
|
2202
|
+
repoCwd: requestedCwd,
|
|
2161
2203
|
worktree,
|
|
2162
2204
|
});
|
|
2163
2205
|
let cleanup: WorktreeCleanup;
|
|
@@ -2174,7 +2216,7 @@ export default function workflows(
|
|
|
2174
2216
|
const reclaimer =
|
|
2175
2217
|
workflowLifecycleTestHooks?.reclaimWorktree ??
|
|
2176
2218
|
reclaimWorktree;
|
|
2177
|
-
cleanup = await reclaimer(
|
|
2219
|
+
cleanup = await reclaimer(requestedCwd, worktree).catch(
|
|
2178
2220
|
(error): WorktreeCleanup => ({
|
|
2179
2221
|
removed: false,
|
|
2180
2222
|
branchDeleted: false,
|
|
@@ -250,8 +250,11 @@ export class AgentProgressProjection {
|
|
|
250
250
|
snapshot(
|
|
251
251
|
toolTimings: ReadonlyMap<string, ProgressToolTiming> = new Map(),
|
|
252
252
|
): AgentProgressProjectionSnapshot {
|
|
253
|
+
// Reserve the initial task, then spend the remaining byte budget on the
|
|
254
|
+
// newest evidence. Forward selection would silently discard final errors
|
|
255
|
+
// after enough large tool results, even below the entry-count limit.
|
|
253
256
|
const selected = this.firstEntry
|
|
254
|
-
? [this.firstEntry, ...this.tailEntries]
|
|
257
|
+
? [this.firstEntry, ...this.tailEntries.slice().reverse()]
|
|
255
258
|
: [];
|
|
256
259
|
const transcript: TranscriptEntry[] = [];
|
|
257
260
|
let totalBytes = 0;
|
|
@@ -277,6 +280,9 @@ export class AgentProgressProjection {
|
|
|
277
280
|
: { timestamp: entry.timestamp }),
|
|
278
281
|
});
|
|
279
282
|
}
|
|
283
|
+
// Budgeting order is not display order: keep the retained tail chronological.
|
|
284
|
+
const newestFirstTail = transcript.splice(1);
|
|
285
|
+
transcript.push(...newestFirstTail.reverse());
|
|
280
286
|
if (transcript.length < this.totalEntries) {
|
|
281
287
|
transcript.push({
|
|
282
288
|
role: "toolResult",
|
|
@@ -50,7 +50,6 @@ import { truncateUtf8 } from "./serialization.ts";
|
|
|
50
50
|
import { bindWorkflowToolRenderer } from "./tool-renderer.ts";
|
|
51
51
|
|
|
52
52
|
const AGENT_OUTPUT_MAX_BYTES = 64 * 1024;
|
|
53
|
-
export const MODEL_PROGRESS_TIMEOUT_MS = 45_000;
|
|
54
53
|
|
|
55
54
|
export type WorkflowModel = NonNullable<ExtensionContext["model"]>;
|
|
56
55
|
export type ThinkingLevel = ReturnType<ExtensionAPI["getThinkingLevel"]>;
|
|
@@ -111,8 +110,6 @@ export interface RunAgentOptions {
|
|
|
111
110
|
replayFilesystemBoundary?: ReplayFilesystemBoundaryOptions;
|
|
112
111
|
/** Test-only override for the per-tool execution timeout. */
|
|
113
112
|
toolCallTimeoutMs?: number;
|
|
114
|
-
/** Test-only override for the per-provider-turn model-progress timeout. */
|
|
115
|
-
modelProgressTimeoutMs?: number;
|
|
116
113
|
/** Test-only override for the end-to-end abort/shutdown deadline. */
|
|
117
114
|
shutdownTimeoutMs?: number;
|
|
118
115
|
/** Test seam for lifecycle races; production always uses createAgentSession. */
|
|
@@ -245,119 +242,6 @@ function errorText(error: unknown): string {
|
|
|
245
242
|
);
|
|
246
243
|
}
|
|
247
244
|
|
|
248
|
-
function formatTimeout(timeoutMs: number) {
|
|
249
|
-
return timeoutMs % 1_000 === 0
|
|
250
|
-
? `${timeoutMs / 1_000} seconds`
|
|
251
|
-
: `${timeoutMs} ms`;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
export function resolveModelProgressTimeoutMs(
|
|
255
|
-
settingsManager: SettingsManager,
|
|
256
|
-
override?: number,
|
|
257
|
-
) {
|
|
258
|
-
if (override !== undefined) return override;
|
|
259
|
-
const configured =
|
|
260
|
-
settingsManager.getProjectSettings().httpIdleTimeoutMs ??
|
|
261
|
-
settingsManager.getGlobalSettings().httpIdleTimeoutMs;
|
|
262
|
-
return typeof configured === "number" && Number.isFinite(configured)
|
|
263
|
-
? Math.max(MODEL_PROGRESS_TIMEOUT_MS, Math.floor(configured))
|
|
264
|
-
: MODEL_PROGRESS_TIMEOUT_MS;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/** Abort any provider turn that stops producing model-visible progress. */
|
|
268
|
-
export function createModelProgressWatchdog(
|
|
269
|
-
onTimeout: (error: Error) => Promise<unknown>,
|
|
270
|
-
options: { timeoutMs?: number; model?: string } = {},
|
|
271
|
-
) {
|
|
272
|
-
const timeoutMs = options.timeoutMs ?? MODEL_PROGRESS_TIMEOUT_MS;
|
|
273
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
274
|
-
let activeTurn = false;
|
|
275
|
-
let closed = false;
|
|
276
|
-
let rejectTimeout!: (error: Error) => void;
|
|
277
|
-
const timeout = new Promise<never>((_resolve, reject) => {
|
|
278
|
-
rejectTimeout = reject;
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
const clear = () => {
|
|
282
|
-
if (timer) clearTimeout(timer);
|
|
283
|
-
timer = undefined;
|
|
284
|
-
};
|
|
285
|
-
const schedule = () => {
|
|
286
|
-
clear();
|
|
287
|
-
if (!activeTurn || closed) return;
|
|
288
|
-
// This timer owns the awaited watchdog outcome. Keep it referenced so a
|
|
289
|
-
// short-lived Node 22 process cannot exit with the promise still pending.
|
|
290
|
-
timer = setTimeout(() => {
|
|
291
|
-
timer = undefined;
|
|
292
|
-
activeTurn = false;
|
|
293
|
-
closed = true;
|
|
294
|
-
const model = options.model ? ` for ${options.model}` : "";
|
|
295
|
-
const error = new Error(
|
|
296
|
-
`Agent provider turn${model} produced no model-visible progress for ${formatTimeout(timeoutMs)}; the provider request may be stalled. Retry the workflow.`,
|
|
297
|
-
);
|
|
298
|
-
rejectTimeout(error);
|
|
299
|
-
try {
|
|
300
|
-
void onTimeout(error).catch(() => {});
|
|
301
|
-
} catch {
|
|
302
|
-
// The timeout result remains authoritative even if abort throws before
|
|
303
|
-
// returning its promise; bounded shutdown below gets another chance.
|
|
304
|
-
}
|
|
305
|
-
}, timeoutMs);
|
|
306
|
-
};
|
|
307
|
-
const armTurn = () => {
|
|
308
|
-
if (closed) return;
|
|
309
|
-
activeTurn = true;
|
|
310
|
-
schedule();
|
|
311
|
-
};
|
|
312
|
-
const markProgress = () => {
|
|
313
|
-
if (!activeTurn || closed) return;
|
|
314
|
-
schedule();
|
|
315
|
-
};
|
|
316
|
-
const completeTurn = () => {
|
|
317
|
-
activeTurn = false;
|
|
318
|
-
clear();
|
|
319
|
-
};
|
|
320
|
-
const cancel = () => {
|
|
321
|
-
closed = true;
|
|
322
|
-
activeTurn = false;
|
|
323
|
-
clear();
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
return {
|
|
327
|
-
armTurn,
|
|
328
|
-
markProgress,
|
|
329
|
-
completeTurn,
|
|
330
|
-
cancel,
|
|
331
|
-
async waitFor<T>(operation: Promise<T>) {
|
|
332
|
-
try {
|
|
333
|
-
return await Promise.race([operation, timeout]);
|
|
334
|
-
} finally {
|
|
335
|
-
cancel();
|
|
336
|
-
}
|
|
337
|
-
},
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function isModelVisibleProgress(event: AgentSessionEvent) {
|
|
342
|
-
if (event.type !== "message_update" || event.message.role !== "assistant") {
|
|
343
|
-
return false;
|
|
344
|
-
}
|
|
345
|
-
// Raw transport heartbeats never become AgentSession events. Empty stream,
|
|
346
|
-
// text, and thinking starts likewise cannot keep a provider turn alive.
|
|
347
|
-
const update = event.assistantMessageEvent;
|
|
348
|
-
if (
|
|
349
|
-
update.type === "text_delta" ||
|
|
350
|
-
update.type === "thinking_delta" ||
|
|
351
|
-
update.type === "toolcall_delta"
|
|
352
|
-
) {
|
|
353
|
-
return update.delta.length > 0;
|
|
354
|
-
}
|
|
355
|
-
if (update.type === "text_end" || update.type === "thinking_end") {
|
|
356
|
-
return update.content.length > 0;
|
|
357
|
-
}
|
|
358
|
-
return update.type === "toolcall_start" || update.type === "toolcall_end";
|
|
359
|
-
}
|
|
360
|
-
|
|
361
245
|
export async function runAgent(
|
|
362
246
|
options: RunAgentOptions,
|
|
363
247
|
): Promise<AgentOutcome> {
|
|
@@ -367,8 +251,6 @@ export async function runAgent(
|
|
|
367
251
|
let session: AgentSession | undefined;
|
|
368
252
|
let unsubscribeToolGuards: (() => void) | undefined;
|
|
369
253
|
let aborted = false;
|
|
370
|
-
let terminalCause: "abort" | "model-progress-timeout" | undefined;
|
|
371
|
-
let modelProgressTimeoutMessage: string | undefined;
|
|
372
254
|
let abortOperation: Promise<unknown> | undefined;
|
|
373
255
|
let rejectForAbort: ((error: Error) => void) | undefined;
|
|
374
256
|
let rejectForProjectionFailure: ((error: Error) => void) | undefined;
|
|
@@ -389,7 +271,6 @@ export async function runAgent(
|
|
|
389
271
|
const onAbort = () => {
|
|
390
272
|
if (aborted) return;
|
|
391
273
|
aborted = true;
|
|
392
|
-
terminalCause ??= "abort";
|
|
393
274
|
if (session) {
|
|
394
275
|
try {
|
|
395
276
|
abortOperation ??= session.abort();
|
|
@@ -594,10 +475,6 @@ export async function runAgent(
|
|
|
594
475
|
});
|
|
595
476
|
};
|
|
596
477
|
|
|
597
|
-
let armModelProgress = () => {};
|
|
598
|
-
let markModelProgress = () => {};
|
|
599
|
-
let completeModelTurn = () => {};
|
|
600
|
-
let cancelModelProgressWatchdog = () => {};
|
|
601
478
|
let compactionReconcileQueued = false;
|
|
602
479
|
const queueCompactionReconcile = () => {
|
|
603
480
|
if (compactionReconcileQueued) return;
|
|
@@ -625,7 +502,6 @@ export async function runAgent(
|
|
|
625
502
|
};
|
|
626
503
|
const unsubscribe = childSession.subscribe((event) => {
|
|
627
504
|
if (settled) return;
|
|
628
|
-
if (event.type === "turn_start") armModelProgress();
|
|
629
505
|
if (event.type === "tool_execution_start") {
|
|
630
506
|
toolRenderer.start(
|
|
631
507
|
event.toolCallId,
|
|
@@ -648,10 +524,6 @@ export async function runAgent(
|
|
|
648
524
|
event.isError,
|
|
649
525
|
);
|
|
650
526
|
}
|
|
651
|
-
if (isModelVisibleProgress(event)) markModelProgress();
|
|
652
|
-
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
653
|
-
completeModelTurn();
|
|
654
|
-
}
|
|
655
527
|
if (event.type === "message_end") {
|
|
656
528
|
assistantSettlement = observeAssistantSettlement(
|
|
657
529
|
assistantSettlement,
|
|
@@ -686,32 +558,10 @@ export async function runAgent(
|
|
|
686
558
|
captureToolRenderData(childSession.messages);
|
|
687
559
|
snapshotProjection();
|
|
688
560
|
if (!aborted) {
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
terminalCause ??= "model-progress-timeout";
|
|
692
|
-
if (terminalCause === "model-progress-timeout") {
|
|
693
|
-
modelProgressTimeoutMessage ??= error.message;
|
|
694
|
-
}
|
|
695
|
-
abortOperation ??= childSession.abort();
|
|
696
|
-
void abortOperation.catch(() => {});
|
|
697
|
-
return abortOperation;
|
|
698
|
-
},
|
|
699
|
-
{
|
|
700
|
-
timeoutMs: resolveModelProgressTimeoutMs(
|
|
701
|
-
options.settingsManager,
|
|
702
|
-
options.modelProgressTimeoutMs,
|
|
703
|
-
),
|
|
704
|
-
model: modelId,
|
|
705
|
-
},
|
|
706
|
-
);
|
|
707
|
-
armModelProgress = watchdog.armTurn;
|
|
708
|
-
markModelProgress = watchdog.markProgress;
|
|
709
|
-
completeModelTurn = watchdog.completeTurn;
|
|
710
|
-
cancelModelProgressWatchdog = watchdog.cancel;
|
|
561
|
+
// Pi owns transport liveness and retries. Quiet model output is not
|
|
562
|
+
// evidence of a stalled request (thinking and retry backoff can be silent).
|
|
711
563
|
await Promise.race([
|
|
712
|
-
|
|
713
|
-
childSession.prompt(buildWorkflowAgentPrompt(options.prompt)),
|
|
714
|
-
),
|
|
564
|
+
childSession.prompt(buildWorkflowAgentPrompt(options.prompt)),
|
|
715
565
|
abortRace,
|
|
716
566
|
projectionFailureRace,
|
|
717
567
|
]);
|
|
@@ -719,7 +569,6 @@ export async function runAgent(
|
|
|
719
569
|
} catch (error) {
|
|
720
570
|
promptErrorMessage ??= errorText(error);
|
|
721
571
|
} finally {
|
|
722
|
-
cancelModelProgressWatchdog();
|
|
723
572
|
options.signal?.removeEventListener("abort", onAbort);
|
|
724
573
|
settled = true;
|
|
725
574
|
unsubscribe();
|
|
@@ -758,11 +607,7 @@ export async function runAgent(
|
|
|
758
607
|
? `Cleanup failed: ${cleanupErrors.join("; ")}`
|
|
759
608
|
: undefined;
|
|
760
609
|
|
|
761
|
-
if (
|
|
762
|
-
terminalCause === "abort" ||
|
|
763
|
-
(terminalCause === undefined &&
|
|
764
|
-
assistantSettlement?.stopReason === "aborted")
|
|
765
|
-
) {
|
|
610
|
+
if (aborted || assistantSettlement?.stopReason === "aborted") {
|
|
766
611
|
return {
|
|
767
612
|
ok: false,
|
|
768
613
|
output,
|
|
@@ -779,9 +624,7 @@ export async function runAgent(
|
|
|
779
624
|
}
|
|
780
625
|
|
|
781
626
|
const failureMessage =
|
|
782
|
-
(
|
|
783
|
-
? modelProgressTimeoutMessage
|
|
784
|
-
: agentFailureMessage(assistantSettlement, promptErrorMessage)) ??
|
|
627
|
+
agentFailureMessage(assistantSettlement, promptErrorMessage) ??
|
|
785
628
|
cleanupError;
|
|
786
629
|
if (failureMessage !== undefined) {
|
|
787
630
|
return {
|