@tt-a1i/openpi 0.1.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 +643 -0
- package/SETUP.md +74 -0
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/assets/openpi-package.png +0 -0
- package/assets/readme-hero-mobile.svg +72 -0
- package/assets/readme-hero.svg +118 -0
- package/assets/readme-runtime-mobile.svg +91 -0
- package/assets/readme-runtime.svg +111 -0
- package/extensions/ask-user/handoff.ts +205 -0
- package/extensions/ask-user/index.ts +1110 -0
- package/extensions/ask-user/limits.ts +89 -0
- package/extensions/ask-user/prompt.ts +76 -0
- package/extensions/background-terminals/index.ts +653 -0
- package/extensions/background-terminals/src/domain.ts +99 -0
- package/extensions/background-terminals/src/manager.ts +989 -0
- package/extensions/background-terminals/src/output.ts +84 -0
- package/extensions/background-terminals/src/prompt.ts +195 -0
- package/extensions/background-terminals/src/result-delivery.ts +43 -0
- package/extensions/background-terminals/src/runtime.ts +36 -0
- package/extensions/background-terminals/src/ui/output-view.ts +55 -0
- package/extensions/background-terminals/src/ui/ps.ts +642 -0
- package/extensions/background-terminals/src/ui/tool-result.ts +146 -0
- package/extensions/background-terminals/src/watch.ts +192 -0
- package/extensions/context-pivot/index.ts +222 -0
- package/extensions/copy-all/index.ts +65 -0
- package/extensions/cron/index.ts +173 -0
- package/extensions/cron/schedule.ts +127 -0
- package/extensions/file-mutation-display/index.ts +105 -0
- package/extensions/file-mutation-display/render.ts +107 -0
- package/extensions/file-search/index.ts +515 -0
- package/extensions/file-search/src/args.ts +129 -0
- package/extensions/file-search/src/binaries.ts +419 -0
- package/extensions/file-search/src/output.ts +142 -0
- package/extensions/file-search/src/process.ts +309 -0
- package/extensions/file-search/src/prompt.ts +53 -0
- package/extensions/git-info/index.ts +272 -0
- package/extensions/git-info/src/changed-files-view.ts +414 -0
- package/extensions/git-info/src/process.ts +107 -0
- package/extensions/git-info/src/refresh-coordinator.ts +13 -0
- package/extensions/git-info/src/runtime.ts +28 -0
- package/extensions/goal/controller.ts +794 -0
- package/extensions/goal/index.ts +521 -0
- package/extensions/goal/prompts.ts +122 -0
- package/extensions/goal/state.ts +763 -0
- package/extensions/goal/ui.ts +158 -0
- package/extensions/model-info/index.ts +234 -0
- package/extensions/plan-mode/bash-policy.ts +313 -0
- package/extensions/plan-mode/index.ts +539 -0
- package/extensions/post-edit/index.ts +129 -0
- package/extensions/sessions/LICENSE.upstream +21 -0
- package/extensions/sessions/git-stats.ts +226 -0
- package/extensions/sessions/index.ts +1092 -0
- package/extensions/sessions/sessions.ts +385 -0
- package/extensions/setup/index.ts +408 -0
- package/extensions/shared/activity-status.ts +65 -0
- package/extensions/shared/below-editor-navigation.ts +343 -0
- package/extensions/shared/child-session.ts +352 -0
- package/extensions/shared/context-utilization.ts +47 -0
- package/extensions/shared/dashboard-state.ts +102 -0
- package/extensions/shared/plan-mode-state.ts +65 -0
- package/extensions/shared/setup-config.ts +971 -0
- package/extensions/shared/subagent-roles.ts +22 -0
- package/extensions/shared/terminal-text.ts +38 -0
- package/extensions/shared/tool-call-timeout.ts +104 -0
- package/extensions/shared/worktree.ts +526 -0
- package/extensions/subagents/index.ts +1225 -0
- package/extensions/subagents/navigation.ts +121 -0
- package/extensions/subagents/src/agent-types.ts +543 -0
- package/extensions/subagents/src/backend.ts +63 -0
- package/extensions/subagents/src/backends/pi.ts +493 -0
- package/extensions/subagents/src/backends/stub.ts +296 -0
- package/extensions/subagents/src/by-the-way.ts +21 -0
- package/extensions/subagents/src/domain.ts +271 -0
- package/extensions/subagents/src/format.ts +48 -0
- package/extensions/subagents/src/manager.ts +769 -0
- package/extensions/subagents/src/prompt.ts +190 -0
- package/extensions/subagents/src/result-delivery.ts +20 -0
- package/extensions/subagents/src/runtime.ts +51 -0
- package/extensions/subagents/src/ui/takeover.ts +615 -0
- package/extensions/subagents/src/ui/transcript.ts +293 -0
- package/extensions/subagents/src/ui/wait-result.ts +89 -0
- package/extensions/suggestions/index.ts +172 -0
- package/extensions/suggestions/src/config.ts +12 -0
- package/extensions/suggestions/src/predictor.ts +147 -0
- package/extensions/suggestions/src/prompt.ts +20 -0
- package/extensions/suggestions/src/transcript.ts +233 -0
- package/extensions/suggestions/src/ui.ts +224 -0
- package/extensions/tasks/index.ts +512 -0
- package/extensions/tasks/tasks.ts +649 -0
- package/extensions/tasks/ui.ts +421 -0
- package/extensions/turn-time/index.ts +61 -0
- package/extensions/ui-customization/footer.ts +512 -0
- package/extensions/ui-customization/index.ts +217 -0
- package/extensions/workflows/acceptance.ts +298 -0
- package/extensions/workflows/artifacts.ts +225 -0
- package/extensions/workflows/controller.ts +210 -0
- package/extensions/workflows/dashboard.ts +1226 -0
- package/extensions/workflows/index.ts +1884 -0
- package/extensions/workflows/journal.ts +188 -0
- package/extensions/workflows/meta.ts +250 -0
- package/extensions/workflows/model.ts +423 -0
- package/extensions/workflows/navigation.ts +93 -0
- package/extensions/workflows/prompt.ts +212 -0
- package/extensions/workflows/replay-safety.ts +577 -0
- package/extensions/workflows/runner.ts +786 -0
- package/extensions/workflows/sandbox-child.cjs +402 -0
- package/extensions/workflows/sandbox.ts +397 -0
- package/extensions/workflows/serialization.ts +162 -0
- package/extensions/workflows/worktree-handoff.ts +216 -0
- package/package.json +87 -0
- package/scripts/prepare-effect-tsgo.mjs +16 -0
- package/skills/background-terminals/SKILL.md +30 -0
- package/skills/subagents/SKILL.md +15 -0
- package/themes/github-dark-default.json +89 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type { TranscriptEntry, WorkflowDetails } from "./model.ts";
|
|
2
|
+
import {
|
|
3
|
+
boundedJournal,
|
|
4
|
+
parseJournal,
|
|
5
|
+
type JournalEntry,
|
|
6
|
+
type WorkflowJournal,
|
|
7
|
+
} from "./journal.ts";
|
|
8
|
+
import {
|
|
9
|
+
safeStringify,
|
|
10
|
+
truncateUtf8,
|
|
11
|
+
writeFileAtomic,
|
|
12
|
+
} from "./serialization.ts";
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
|
|
16
|
+
export const JOURNAL_FILE = "journal.json";
|
|
17
|
+
|
|
18
|
+
const ARTIFACT_TRANSCRIPT_MAX_BYTES = 32 * 1024;
|
|
19
|
+
const ARTIFACT_TRANSCRIPT_ENTRY_MAX_BYTES = 8 * 1024;
|
|
20
|
+
export const WORKFLOW_CHECKPOINT_INTERVAL_MS = 500;
|
|
21
|
+
const ENTRY_TRUNCATION_MARKER = "\n[entry truncated]";
|
|
22
|
+
const TRANSCRIPT_TRUNCATION_MARKER =
|
|
23
|
+
"[artifact transcript truncated: older entries omitted]";
|
|
24
|
+
|
|
25
|
+
function textBytes(text: string) {
|
|
26
|
+
return Buffer.byteLength(text, "utf8");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function boundEntry(entry: TranscriptEntry, maxBytes: number) {
|
|
30
|
+
if (textBytes(entry.text) <= maxBytes) return { ...entry };
|
|
31
|
+
const markerBytes = textBytes(ENTRY_TRUNCATION_MARKER);
|
|
32
|
+
const text =
|
|
33
|
+
maxBytes > markerBytes
|
|
34
|
+
? `${truncateUtf8(entry.text, maxBytes - markerBytes)}${ENTRY_TRUNCATION_MARKER}`
|
|
35
|
+
: truncateUtf8(ENTRY_TRUNCATION_MARKER, maxBytes);
|
|
36
|
+
return { ...entry, text };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Keep the initial prompt plus the newest useful context within the artifact cap. */
|
|
40
|
+
export function boundedArtifactTranscript(
|
|
41
|
+
transcript: TranscriptEntry[],
|
|
42
|
+
options: { maxBytes?: number; entryMaxBytes?: number } = {},
|
|
43
|
+
) {
|
|
44
|
+
if (transcript.length === 0) return [];
|
|
45
|
+
const maxBytes = Math.max(
|
|
46
|
+
256,
|
|
47
|
+
options.maxBytes ?? ARTIFACT_TRANSCRIPT_MAX_BYTES,
|
|
48
|
+
);
|
|
49
|
+
const entryMaxBytes = Math.max(
|
|
50
|
+
64,
|
|
51
|
+
Math.min(
|
|
52
|
+
maxBytes,
|
|
53
|
+
options.entryMaxBytes ?? ARTIFACT_TRANSCRIPT_ENTRY_MAX_BYTES,
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
const bounded = transcript.map((entry) => boundEntry(entry, entryMaxBytes));
|
|
57
|
+
if (
|
|
58
|
+
bounded.reduce((total, entry) => total + textBytes(entry.text), 0) <=
|
|
59
|
+
maxBytes
|
|
60
|
+
) {
|
|
61
|
+
return bounded;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const initialIndex = transcript.findIndex((entry) => entry.role === "user");
|
|
65
|
+
const initial = boundEntry(
|
|
66
|
+
transcript[initialIndex >= 0 ? initialIndex : 0],
|
|
67
|
+
Math.min(entryMaxBytes, maxBytes - textBytes(TRANSCRIPT_TRUNCATION_MARKER)),
|
|
68
|
+
);
|
|
69
|
+
const marker: TranscriptEntry = {
|
|
70
|
+
role: "toolResult",
|
|
71
|
+
name: "transcript",
|
|
72
|
+
text: TRANSCRIPT_TRUNCATION_MARKER,
|
|
73
|
+
};
|
|
74
|
+
let remaining = maxBytes - textBytes(initial.text) - textBytes(marker.text);
|
|
75
|
+
const tail: TranscriptEntry[] = [];
|
|
76
|
+
|
|
77
|
+
for (
|
|
78
|
+
let index = transcript.length - 1;
|
|
79
|
+
index >= 0 && remaining > 0;
|
|
80
|
+
index--
|
|
81
|
+
) {
|
|
82
|
+
if (index === initialIndex || (initialIndex < 0 && index === 0)) continue;
|
|
83
|
+
const entry = boundEntry(
|
|
84
|
+
transcript[index],
|
|
85
|
+
Math.min(entryMaxBytes, remaining),
|
|
86
|
+
);
|
|
87
|
+
tail.push(entry);
|
|
88
|
+
remaining -= textBytes(entry.text);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
tail.reverse();
|
|
92
|
+
return [initial, marker, ...tail];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function writeRunFile(runDir: string, name: string, content: string) {
|
|
96
|
+
writeFileAtomic(path.join(runDir, name), content);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function persistWorkflowJson(
|
|
100
|
+
runDir: string,
|
|
101
|
+
details: WorkflowDetails,
|
|
102
|
+
journal?: readonly JournalEntry[],
|
|
103
|
+
) {
|
|
104
|
+
const transcripts = Object.fromEntries(
|
|
105
|
+
details.agents.map((agent) => [
|
|
106
|
+
agent.index,
|
|
107
|
+
boundedArtifactTranscript(agent.transcript),
|
|
108
|
+
]),
|
|
109
|
+
);
|
|
110
|
+
writeRunFile(
|
|
111
|
+
runDir,
|
|
112
|
+
"transcripts.json",
|
|
113
|
+
safeStringify(transcripts, { maxBytes: 2 * 1024 * 1024 }),
|
|
114
|
+
);
|
|
115
|
+
// Written alongside the rest so it inherits atomic write, 500ms coalescing,
|
|
116
|
+
// and the final flush. Only present once a call has actually succeeded.
|
|
117
|
+
// boundedJournal has already brought this under the cap, and plain
|
|
118
|
+
// JSON.stringify is deliberate: safeStringify would swap an over-cap value
|
|
119
|
+
// for a preview stub, silently turning the journal into something unusable.
|
|
120
|
+
if (journal && journal.length > 0) {
|
|
121
|
+
writeRunFile(
|
|
122
|
+
runDir,
|
|
123
|
+
JOURNAL_FILE,
|
|
124
|
+
JSON.stringify(boundedJournal(journal).journal, null, 2),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (details.result !== undefined) {
|
|
128
|
+
writeRunFile(
|
|
129
|
+
runDir,
|
|
130
|
+
"result.json",
|
|
131
|
+
safeStringify(details.result, { maxBytes: 1024 * 1024 }),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
const compact: WorkflowDetails = {
|
|
135
|
+
...details,
|
|
136
|
+
...(details.result !== undefined
|
|
137
|
+
? { result: "[stored in result.json]", resultArtifact: "result.json" }
|
|
138
|
+
: {}),
|
|
139
|
+
transcriptArtifact: "transcripts.json",
|
|
140
|
+
agents: details.agents.map((agent) => ({ ...agent, transcript: [] })),
|
|
141
|
+
};
|
|
142
|
+
writeRunFile(
|
|
143
|
+
runDir,
|
|
144
|
+
"workflow.json",
|
|
145
|
+
safeStringify(compact, { maxBytes: 1024 * 1024 }),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Coalesce live checkpoints while keeping final persistence synchronous. */
|
|
150
|
+
export function createWorkflowPersistence(
|
|
151
|
+
runDir: string,
|
|
152
|
+
details: WorkflowDetails,
|
|
153
|
+
options: {
|
|
154
|
+
intervalMs?: number;
|
|
155
|
+
persist?: (
|
|
156
|
+
runDir: string,
|
|
157
|
+
details: WorkflowDetails,
|
|
158
|
+
journal?: readonly JournalEntry[],
|
|
159
|
+
) => void;
|
|
160
|
+
/** Read at write time so callers only have to append to their own array. */
|
|
161
|
+
journal?: () => readonly JournalEntry[];
|
|
162
|
+
} = {},
|
|
163
|
+
) {
|
|
164
|
+
const intervalMs = Math.max(
|
|
165
|
+
0,
|
|
166
|
+
options.intervalMs ?? WORKFLOW_CHECKPOINT_INTERVAL_MS,
|
|
167
|
+
);
|
|
168
|
+
const persist = options.persist ?? persistWorkflowJson;
|
|
169
|
+
const readJournal = options.journal;
|
|
170
|
+
let lastPersistedAt = Date.now();
|
|
171
|
+
let dirty = false;
|
|
172
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
173
|
+
|
|
174
|
+
const savePending = () => {
|
|
175
|
+
timer = undefined;
|
|
176
|
+
if (!dirty) return;
|
|
177
|
+
try {
|
|
178
|
+
persist(runDir, details, readJournal?.());
|
|
179
|
+
dirty = false;
|
|
180
|
+
lastPersistedAt = Date.now();
|
|
181
|
+
} catch {
|
|
182
|
+
// Final flush retries and reports persistence failures synchronously.
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
checkpoint(options: { immediate?: boolean } = {}) {
|
|
188
|
+
dirty = true;
|
|
189
|
+
if (options.immediate) {
|
|
190
|
+
if (timer) clearTimeout(timer);
|
|
191
|
+
timer = undefined;
|
|
192
|
+
savePending();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (timer) return;
|
|
196
|
+
const delay = Math.max(0, intervalMs - (Date.now() - lastPersistedAt));
|
|
197
|
+
if (delay === 0) {
|
|
198
|
+
savePending();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
timer = setTimeout(savePending, delay);
|
|
202
|
+
},
|
|
203
|
+
flush() {
|
|
204
|
+
if (timer) clearTimeout(timer);
|
|
205
|
+
timer = undefined;
|
|
206
|
+
persist(runDir, details, readJournal?.());
|
|
207
|
+
dirty = false;
|
|
208
|
+
lastPersistedAt = Date.now();
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Read a prior run's replay journal. Any failure (missing run, unreadable or
|
|
215
|
+
* malformed file) yields undefined: resume is an optimization and must never
|
|
216
|
+
* turn into a new way for a run to fail.
|
|
217
|
+
*/
|
|
218
|
+
export function loadJournal(runDir: string): WorkflowJournal | undefined {
|
|
219
|
+
try {
|
|
220
|
+
const raw = fs.readFileSync(path.join(runDir, JOURNAL_FILE), "utf8");
|
|
221
|
+
return parseJournal(JSON.parse(raw));
|
|
222
|
+
} catch {
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_WORKFLOW_CONCURRENCY,
|
|
3
|
+
DEFAULT_WORKFLOW_MAX_AGENT_CALLS,
|
|
4
|
+
MAX_WORKFLOW_AGENT_CALLS,
|
|
5
|
+
MAX_WORKFLOW_CONCURRENCY,
|
|
6
|
+
} from "../shared/setup-config.ts";
|
|
7
|
+
export const RUN_SHUTDOWN_TIMEOUT_MS = 8_000;
|
|
8
|
+
|
|
9
|
+
function abortError(signal: AbortSignal) {
|
|
10
|
+
return signal.reason instanceof Error
|
|
11
|
+
? signal.reason
|
|
12
|
+
: new Error("Workflow was aborted");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class Semaphore {
|
|
16
|
+
private active = 0;
|
|
17
|
+
private readonly limit: number;
|
|
18
|
+
private queue: Array<{
|
|
19
|
+
resolve: () => void;
|
|
20
|
+
reject: (error: Error) => void;
|
|
21
|
+
signal: AbortSignal;
|
|
22
|
+
onAbort: () => void;
|
|
23
|
+
}> = [];
|
|
24
|
+
|
|
25
|
+
constructor(limit: number) {
|
|
26
|
+
this.limit = limit;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
acquire(signal: AbortSignal) {
|
|
30
|
+
if (signal.aborted) return Promise.reject(abortError(signal));
|
|
31
|
+
if (this.active < this.limit) {
|
|
32
|
+
this.active++;
|
|
33
|
+
return Promise.resolve();
|
|
34
|
+
}
|
|
35
|
+
return new Promise<void>((resolve, reject) => {
|
|
36
|
+
const waiter = {
|
|
37
|
+
resolve: () => {
|
|
38
|
+
signal.removeEventListener("abort", onAbort);
|
|
39
|
+
this.active++;
|
|
40
|
+
resolve();
|
|
41
|
+
},
|
|
42
|
+
reject,
|
|
43
|
+
signal,
|
|
44
|
+
onAbort: () => {},
|
|
45
|
+
};
|
|
46
|
+
const onAbort = () => {
|
|
47
|
+
const index = this.queue.indexOf(waiter);
|
|
48
|
+
if (index >= 0) this.queue.splice(index, 1);
|
|
49
|
+
reject(abortError(signal));
|
|
50
|
+
};
|
|
51
|
+
waiter.onAbort = onAbort;
|
|
52
|
+
this.queue.push(waiter);
|
|
53
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
release() {
|
|
58
|
+
this.active = Math.max(0, this.active - 1);
|
|
59
|
+
while (this.queue.length > 0) {
|
|
60
|
+
const waiter = this.queue.shift()!;
|
|
61
|
+
if (waiter.signal.aborted) {
|
|
62
|
+
waiter.signal.removeEventListener("abort", waiter.onAbort);
|
|
63
|
+
waiter.reject(abortError(waiter.signal));
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
waiter.resolve();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
clear() {
|
|
72
|
+
const queued = this.queue;
|
|
73
|
+
this.queue = [];
|
|
74
|
+
for (const waiter of queued) {
|
|
75
|
+
waiter.signal.removeEventListener("abort", waiter.onAbort);
|
|
76
|
+
waiter.reject(abortError(waiter.signal));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Owns every agent task and the run-wide fanout/abort budget. */
|
|
82
|
+
export class RunController {
|
|
83
|
+
private readonly abortController = new AbortController();
|
|
84
|
+
private readonly semaphore: Semaphore;
|
|
85
|
+
private readonly maxAgentCalls: number;
|
|
86
|
+
private readonly tasks = new Set<Promise<unknown>>();
|
|
87
|
+
private callCount = 0;
|
|
88
|
+
private sealed = false;
|
|
89
|
+
private settlePromise?: Promise<boolean>;
|
|
90
|
+
private parentAbort?: () => void;
|
|
91
|
+
private parentSignal?: AbortSignal;
|
|
92
|
+
|
|
93
|
+
constructor(
|
|
94
|
+
parentSignal?: AbortSignal,
|
|
95
|
+
concurrency = DEFAULT_WORKFLOW_CONCURRENCY,
|
|
96
|
+
maxAgentCalls = DEFAULT_WORKFLOW_MAX_AGENT_CALLS,
|
|
97
|
+
) {
|
|
98
|
+
this.semaphore = new Semaphore(
|
|
99
|
+
Math.max(1, Math.min(MAX_WORKFLOW_CONCURRENCY, Math.floor(concurrency))),
|
|
100
|
+
);
|
|
101
|
+
this.maxAgentCalls = Math.max(
|
|
102
|
+
1,
|
|
103
|
+
Math.min(MAX_WORKFLOW_AGENT_CALLS, Math.floor(maxAgentCalls)),
|
|
104
|
+
);
|
|
105
|
+
if (parentSignal) {
|
|
106
|
+
this.parentSignal = parentSignal;
|
|
107
|
+
this.parentAbort = () => this.abort("Parent operation was aborted");
|
|
108
|
+
if (parentSignal.aborted) this.parentAbort();
|
|
109
|
+
else
|
|
110
|
+
parentSignal.addEventListener("abort", this.parentAbort, {
|
|
111
|
+
once: true,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
get signal() {
|
|
117
|
+
return this.abortController.signal;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
get calls() {
|
|
121
|
+
return this.callCount;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
schedule<T>(
|
|
125
|
+
task: (signal: AbortSignal) => Promise<T>,
|
|
126
|
+
invocationSignal?: AbortSignal,
|
|
127
|
+
): Promise<T> {
|
|
128
|
+
if (this.sealed) return Promise.reject(new Error("Workflow is settling"));
|
|
129
|
+
if (this.signal.aborted) return Promise.reject(abortError(this.signal));
|
|
130
|
+
if (this.callCount >= this.maxAgentCalls) {
|
|
131
|
+
return Promise.reject(
|
|
132
|
+
new Error(
|
|
133
|
+
`Workflow exceeded the limit of ${this.maxAgentCalls} agent calls`,
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
this.callCount++;
|
|
138
|
+
|
|
139
|
+
const running = (async () => {
|
|
140
|
+
const taskAbort = new AbortController();
|
|
141
|
+
const onRunAbort = () => taskAbort.abort(this.signal.reason);
|
|
142
|
+
const onInvocationAbort = () => taskAbort.abort(invocationSignal?.reason);
|
|
143
|
+
this.signal.addEventListener("abort", onRunAbort, { once: true });
|
|
144
|
+
invocationSignal?.addEventListener("abort", onInvocationAbort, {
|
|
145
|
+
once: true,
|
|
146
|
+
});
|
|
147
|
+
if (this.signal.aborted) onRunAbort();
|
|
148
|
+
else if (invocationSignal?.aborted) onInvocationAbort();
|
|
149
|
+
|
|
150
|
+
let acquired = false;
|
|
151
|
+
try {
|
|
152
|
+
await this.semaphore.acquire(taskAbort.signal);
|
|
153
|
+
acquired = true;
|
|
154
|
+
if (taskAbort.signal.aborted) throw abortError(taskAbort.signal);
|
|
155
|
+
const result = await task(taskAbort.signal);
|
|
156
|
+
if (invocationSignal?.aborted) throw abortError(invocationSignal);
|
|
157
|
+
return result;
|
|
158
|
+
} finally {
|
|
159
|
+
this.signal.removeEventListener("abort", onRunAbort);
|
|
160
|
+
invocationSignal?.removeEventListener("abort", onInvocationAbort);
|
|
161
|
+
if (acquired) this.semaphore.release();
|
|
162
|
+
}
|
|
163
|
+
})();
|
|
164
|
+
this.tasks.add(running);
|
|
165
|
+
void running.finally(() => this.tasks.delete(running)).catch(() => {});
|
|
166
|
+
return running;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
abort(reason = "Workflow was aborted") {
|
|
170
|
+
if (!this.signal.aborted) this.abortController.abort(new Error(reason));
|
|
171
|
+
this.semaphore.clear();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Seal once and wait a bounded time for the same task snapshot on every caller. */
|
|
175
|
+
settle(options: { abort?: boolean; timeoutMs?: number } = {}) {
|
|
176
|
+
this.sealed = true;
|
|
177
|
+
if (options.abort) this.abort();
|
|
178
|
+
if (this.settlePromise) return this.settlePromise;
|
|
179
|
+
|
|
180
|
+
const tasks = [...this.tasks];
|
|
181
|
+
this.settlePromise = (async () => {
|
|
182
|
+
if (tasks.length === 0) {
|
|
183
|
+
this.detachParent();
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
188
|
+
const timeout = new Promise<false>((resolve) => {
|
|
189
|
+
timer = setTimeout(
|
|
190
|
+
() => resolve(false),
|
|
191
|
+
options.timeoutMs ?? RUN_SHUTDOWN_TIMEOUT_MS,
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
const settled = Promise.allSettled(tasks).then(() => true as const);
|
|
195
|
+
const completed = await Promise.race([settled, timeout]);
|
|
196
|
+
if (timer) clearTimeout(timer);
|
|
197
|
+
this.detachParent();
|
|
198
|
+
return completed;
|
|
199
|
+
})();
|
|
200
|
+
return this.settlePromise;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private detachParent() {
|
|
204
|
+
if (this.parentAbort) {
|
|
205
|
+
this.parentSignal?.removeEventListener("abort", this.parentAbort);
|
|
206
|
+
}
|
|
207
|
+
this.parentAbort = undefined;
|
|
208
|
+
this.parentSignal = undefined;
|
|
209
|
+
}
|
|
210
|
+
}
|