agents 0.29.0 → 0.30.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 +3 -2
- package/dist/browser/tools/pi.d.ts +6 -2
- package/dist/browser/tools/pi.js +6 -2
- package/dist/browser/tools/pi.js.map +1 -1
- package/dist/chat-sdk/index.js +1 -1
- package/dist/cli.js +46 -45
- package/dist/experimental/channels/web/index.js +1 -1
- package/dist/harness/container/index.js +1 -1
- package/dist/harness/opencode/index.js +1 -1
- package/dist/harness/pi/index.d.ts +60 -35
- package/dist/harness/pi/index.js +271 -119
- package/dist/harness/pi/index.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/lifecycle/index.js +1 -1
- package/dist/{lifecycle-ClT4DmF2.js → lifecycle-v1rKHV2C.js} +57 -17
- package/dist/lifecycle-v1rKHV2C.js.map +1 -0
- package/dist/mcp/index.js +1 -1
- package/dist/models/pi-ai/index.js +2 -2
- package/dist/models/pi-ai/index.js.map +1 -1
- package/dist/{src-6lr7MtM_.js → src-DfLzdwlK.js} +3 -3
- package/dist/{src-6lr7MtM_.js.map → src-DfLzdwlK.js.map} +1 -1
- package/dist/web/fetch/tools/pi.d.ts +10 -6
- package/dist/web/fetch/tools/pi.js +10 -6
- package/dist/web/fetch/tools/pi.js.map +1 -1
- package/dist/web/search/tools/pi.d.ts +10 -6
- package/dist/web/search/tools/pi.js +10 -6
- package/dist/web/search/tools/pi.js.map +1 -1
- package/dist/websockets/index.js +1 -1
- package/dist/{websockets-CqxnntmE.js → websockets-CyoeosdO.js} +2 -2
- package/dist/{websockets-CqxnntmE.js.map → websockets-CyoeosdO.js.map} +1 -1
- package/dist/workflows.js +1 -1
- package/docs/get-current-agent.md +4 -7
- package/docs/harnesses/pi.md +62 -28
- package/docs/human-in-the-loop.md +3 -7
- package/docs/index.md +2 -2
- package/docs/lifecycle.md +7 -1
- package/docs/mcp-client.md +2 -28
- package/docs/mcp-servers.md +121 -414
- package/docs/mcp-transports.md +30 -289
- package/docs/models-pi-ai.md +5 -1
- package/docs/routing.md +0 -2
- package/docs/securing-mcp-servers.md +7 -2
- package/package.json +11 -6
- package/dist/lifecycle-ClT4DmF2.js.map +0 -1
package/dist/harness/pi/index.js
CHANGED
|
@@ -1,30 +1,12 @@
|
|
|
1
1
|
import { i as _classPrivateFieldInitSpec, n as _classPrivateFieldSet2, r as _assertClassBrand, t as _classPrivateFieldGet2 } from "../../classPrivateFieldGet2-DZBYAB34.js";
|
|
2
2
|
import { t as _classPrivateMethodInitSpec } from "../../classPrivateMethodInitSpec-qMjJ6sHQ.js";
|
|
3
3
|
import { t as LifecycleCapability } from "../../capability-DBAKc6UZ.js";
|
|
4
|
-
import "../../lifecycle-
|
|
4
|
+
import "../../lifecycle-v1rKHV2C.js";
|
|
5
|
+
import { isDurableObjectMemoryLimitReset } from "../../retries.js";
|
|
5
6
|
import { Type } from "@earendil-works/pi-ai";
|
|
6
7
|
import { LiveDoc, ROOT_CONVERSATION_ID, watchEvents } from "@earendil-works/pi-durable";
|
|
8
|
+
import { BACKGROUND_CONTEXT, withAbortSignal } from "@earendil-works/chord/context";
|
|
7
9
|
import { openDurableObjectSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/cloudflare";
|
|
8
|
-
//#region src/harness/pi/context.ts
|
|
9
|
-
/** chord's `BACKGROUND_CONTEXT`: no values, never cancelled. */
|
|
10
|
-
const BACKGROUND_CONTEXT = Object.freeze({
|
|
11
|
-
abortSignal: void 0,
|
|
12
|
-
value: () => void 0,
|
|
13
|
-
toString: () => "[Context pi-harness background]"
|
|
14
|
-
});
|
|
15
|
-
/**
|
|
16
|
-
* chord's `withAbortSignal`: a context cancelled by either the parent's
|
|
17
|
-
* signal or `signal`, carrying the parent's values.
|
|
18
|
-
*/
|
|
19
|
-
function withAbortSignal(signal, parent) {
|
|
20
|
-
const abortSignal = parent.abortSignal === void 0 ? signal : AbortSignal.any([parent.abortSignal, signal]);
|
|
21
|
-
return Object.freeze({
|
|
22
|
-
abortSignal,
|
|
23
|
-
value: parent.value.bind(parent),
|
|
24
|
-
toString: () => `${parent}.WithAbortSignal`
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
//#endregion
|
|
28
10
|
//#region src/harness/pi/session-store.ts
|
|
29
11
|
const DEFAULT_PREFIX = "pi_";
|
|
30
12
|
/** Open pi-durable's storage over this object's SQLite database. */
|
|
@@ -39,15 +21,10 @@ const BG = BACKGROUND_CONTEXT;
|
|
|
39
21
|
/** The root session's id. */
|
|
40
22
|
const ROOT_SESSION = String(ROOT_CONVERSATION_ID);
|
|
41
23
|
/**
|
|
42
|
-
* The
|
|
43
|
-
*
|
|
24
|
+
* The wake timings. They suit a real deployment; only tests change them,
|
|
25
|
+
* with `setWakeTimingForTests`, so a suite does not sit on the real
|
|
44
26
|
* heartbeat.
|
|
45
27
|
*
|
|
46
|
-
* A pi long wait further away than this is handed to the alarm, so the
|
|
47
|
-
* alarm, not pi's in-memory timer, is what wakes the object.
|
|
48
|
-
*/
|
|
49
|
-
const SLEEP_THRESHOLD_MS = 6e4;
|
|
50
|
-
/**
|
|
51
28
|
* Longest one wake waits on pi. It waits inside an alarm invocation, which
|
|
52
29
|
* has a 15 minute wall-time limit, so a longer run is waited on across
|
|
53
30
|
* several alarms.
|
|
@@ -59,13 +36,28 @@ const WAIT_BUDGET_MS = 10 * 6e4;
|
|
|
59
36
|
* job is still due and its alarm restarts the object.
|
|
60
37
|
*/
|
|
61
38
|
const HEARTBEAT_MS = 3e4;
|
|
39
|
+
/**
|
|
40
|
+
* How often a wait re-reads `pi.wake()` when the factory did not pass
|
|
41
|
+
* `onWake`, so nothing tells the harness when it changes.
|
|
42
|
+
*/
|
|
43
|
+
const POLL_MS = 1e3;
|
|
44
|
+
/**
|
|
45
|
+
* The longest a wake job whose dispatch keeps failing waits before it tries
|
|
46
|
+
* again. Its delay starts at the heartbeat and doubles up to this.
|
|
47
|
+
*/
|
|
48
|
+
const MAX_WAKE_BACKOFF_MS = 10 * 6e4;
|
|
49
|
+
/**
|
|
50
|
+
* Wake dispatches that failed in a row. Stored, so the backoff holds across
|
|
51
|
+
* restarts, and outside the job: a push that saved it in the job's payload
|
|
52
|
+
* could overwrite a submit's due-now push.
|
|
53
|
+
*/
|
|
54
|
+
const WAKE_ERRORS_KEY = "pi-harness:wake-errors";
|
|
62
55
|
const WAKE_FN = "wake";
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
56
|
+
/**
|
|
57
|
+
* The one wake job. Before Pi 1.4 there was one per session, with ids
|
|
58
|
+
* `pi-wake:<session>`; a left-over one hands over to this one.
|
|
59
|
+
*/
|
|
60
|
+
const WAKE_JOB_ID = "pi-wake";
|
|
69
61
|
/** pi-durable's stored reference to a pi-ai model. */
|
|
70
62
|
function modelRef(model) {
|
|
71
63
|
return {
|
|
@@ -88,13 +80,18 @@ function signalContext(signal) {
|
|
|
88
80
|
return signal ? withAbortSignal(signal, BG) : BG;
|
|
89
81
|
}
|
|
90
82
|
var _options = /* @__PURE__ */ new WeakMap();
|
|
91
|
-
var
|
|
83
|
+
var _waiting = /* @__PURE__ */ new WeakMap();
|
|
84
|
+
var _changed = /* @__PURE__ */ new WeakMap();
|
|
92
85
|
var _admitting = /* @__PURE__ */ new WeakMap();
|
|
93
|
-
var
|
|
86
|
+
var _onWakeWired = /* @__PURE__ */ new WeakMap();
|
|
94
87
|
var _waitBudgetMs = /* @__PURE__ */ new WeakMap();
|
|
95
88
|
var _heartbeatMs = /* @__PURE__ */ new WeakMap();
|
|
89
|
+
var _pollMs = /* @__PURE__ */ new WeakMap();
|
|
96
90
|
var _opening = /* @__PURE__ */ new WeakMap();
|
|
91
|
+
var _lastClose = /* @__PURE__ */ new WeakMap();
|
|
92
|
+
var _closing = /* @__PURE__ */ new WeakMap();
|
|
97
93
|
var _PiHarness_brand = /* @__PURE__ */ new WeakSet();
|
|
94
|
+
var _onWake = /* @__PURE__ */ new WeakMap();
|
|
98
95
|
/**
|
|
99
96
|
* pi-durable hosted in a Durable Object, behind a small harness interface:
|
|
100
97
|
* `harness.prompt()`, `harness.sessions`, `harness.session(id)`. How a session reaches a
|
|
@@ -107,12 +104,14 @@ var _PiHarness_brand = /* @__PURE__ */ new WeakSet();
|
|
|
107
104
|
*
|
|
108
105
|
* What pi cannot do on a Durable Object is wake itself: its scheduler runs
|
|
109
106
|
* in memory, and an evicted object has no memory. The harness is that wake,
|
|
110
|
-
* with one Lifecycle job
|
|
111
|
-
* after the
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
107
|
+
* with one Lifecycle job driven by `pi.wake()`. Input goes to pi once, in
|
|
108
|
+
* `submit()`, after the job is scheduled. While pi has work running, the
|
|
109
|
+
* job waits on it, rescheduling itself as a heartbeat. While pi's tasks
|
|
110
|
+
* sleep, the job is due when the first one wakes; pi tells the harness
|
|
111
|
+
* through the `onWake` the factory passes to `Harness.open`. When pi has
|
|
112
|
+
* nothing to do, the job completes. An eviction mid-run leaves the job due,
|
|
113
|
+
* so its alarm restarts the object, pi reopens and resumes its own tasks,
|
|
114
|
+
* and the job waits again.
|
|
116
115
|
*
|
|
117
116
|
* @beta The API may change between releases.
|
|
118
117
|
*/
|
|
@@ -121,25 +120,54 @@ var PiHarness = class extends LifecycleCapability {
|
|
|
121
120
|
super("pi-harness");
|
|
122
121
|
_classPrivateMethodInitSpec(this, _PiHarness_brand);
|
|
123
122
|
_classPrivateFieldInitSpec(this, _options, void 0);
|
|
124
|
-
_classPrivateFieldInitSpec(this,
|
|
125
|
-
_classPrivateFieldInitSpec(this,
|
|
126
|
-
_classPrivateFieldInitSpec(this,
|
|
123
|
+
_classPrivateFieldInitSpec(this, _waiting, void 0);
|
|
124
|
+
_classPrivateFieldInitSpec(this, _changed, void 0);
|
|
125
|
+
_classPrivateFieldInitSpec(this, _admitting, 0);
|
|
126
|
+
_classPrivateFieldInitSpec(this, _onWakeWired, false);
|
|
127
127
|
_classPrivateFieldInitSpec(this, _waitBudgetMs, WAIT_BUDGET_MS);
|
|
128
128
|
_classPrivateFieldInitSpec(this, _heartbeatMs, HEARTBEAT_MS);
|
|
129
|
+
_classPrivateFieldInitSpec(this, _pollMs, POLL_MS);
|
|
129
130
|
_classPrivateFieldInitSpec(this, _opening, void 0);
|
|
131
|
+
_classPrivateFieldInitSpec(this, _lastClose, Promise.resolve());
|
|
132
|
+
_classPrivateFieldInitSpec(this, _closing, /* @__PURE__ */ new WeakSet());
|
|
133
|
+
_classPrivateFieldInitSpec(this, _onWake, (wake) => {
|
|
134
|
+
_classPrivateFieldSet2(_onWakeWired, this, true);
|
|
135
|
+
_classPrivateFieldGet2(_changed, this)?.call(this);
|
|
136
|
+
if (_classPrivateFieldGet2(_waiting, this)) return;
|
|
137
|
+
_assertClassBrand(_PiHarness_brand, this, _wakeFor).call(this, wake).catch((error) => _assertClassBrand(_PiHarness_brand, this, _wakeError).call(this, error));
|
|
138
|
+
});
|
|
130
139
|
_classPrivateFieldSet2(_options, this, options);
|
|
131
140
|
this.sessions = new PiSessions(this);
|
|
132
141
|
}
|
|
133
142
|
async onStart(_context) {
|
|
134
|
-
|
|
135
|
-
const inspection = await pi.inspect(BG);
|
|
136
|
-
const sessions = new Set(inspection.tasks.map((task) => String(task.record.conversationId)));
|
|
137
|
-
for (const session of sessions) await _assertClassBrand(_PiHarness_brand, this, _wake).call(this, session);
|
|
143
|
+
await _assertClassBrand(_PiHarness_brand, this, _open).call(this);
|
|
138
144
|
}
|
|
139
145
|
async onJob(context) {
|
|
140
146
|
if (context.job.fn !== WAKE_FN) return;
|
|
141
|
-
|
|
142
|
-
if (
|
|
147
|
+
let outcome;
|
|
148
|
+
if (context.job.id !== WAKE_JOB_ID) {
|
|
149
|
+
await _assertClassBrand(_PiHarness_brand, this, _wake).call(this);
|
|
150
|
+
outcome = void 0;
|
|
151
|
+
} else outcome = await _assertClassBrand(_PiHarness_brand, this, _wakeStep).call(this);
|
|
152
|
+
const kv = this.lifecycle.storage.kv;
|
|
153
|
+
if (kv.get(WAKE_ERRORS_KEY) !== void 0) kv.delete(WAKE_ERRORS_KEY);
|
|
154
|
+
return outcome;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* A wake dispatch that failed through Lifecycle's retries, as when pi
|
|
158
|
+
* cannot be opened. Lifecycle would complete the job, and pi's work would
|
|
159
|
+
* never be woken again, so try again later: at the heartbeat, doubling
|
|
160
|
+
* while the failures continue, up to 10 minutes.
|
|
161
|
+
*/
|
|
162
|
+
onJobError(context, error) {
|
|
163
|
+
if (context.job.fn !== WAKE_FN) return;
|
|
164
|
+
console.error(`pi wake job "${context.job.id}" failed after ${context.attempt} attempts; retrying with backoff`, error);
|
|
165
|
+
_assertClassBrand(_PiHarness_brand, this, _wakeError).call(this, error);
|
|
166
|
+
const kv = this.lifecycle.storage.kv;
|
|
167
|
+
const errors = (kv.get(WAKE_ERRORS_KEY) ?? 0) + 1;
|
|
168
|
+
kv.put(WAKE_ERRORS_KEY, errors);
|
|
169
|
+
const delay = Math.min(_classPrivateFieldGet2(_heartbeatMs, this) * 2 ** (errors - 1), MAX_WAKE_BACKOFF_MS);
|
|
170
|
+
return { rescheduleAt: Date.now() + delay };
|
|
143
171
|
}
|
|
144
172
|
/** Close pi's in-memory resources. Durable state is untouched. */
|
|
145
173
|
async dispose() {
|
|
@@ -163,6 +191,13 @@ var PiHarness = class extends LifecycleCapability {
|
|
|
163
191
|
abort(options = {}) {
|
|
164
192
|
return this.session(options.session).abort(options.operationId);
|
|
165
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Run again the tasks pi's crash-loop guard parked in a session's work.
|
|
196
|
+
* False when nothing was parked.
|
|
197
|
+
*/
|
|
198
|
+
retry(options = {}) {
|
|
199
|
+
return this.session(options.session).retry();
|
|
200
|
+
}
|
|
166
201
|
wait(operationId, options = {}) {
|
|
167
202
|
return this.session(options.session).wait(operationId, options.signal);
|
|
168
203
|
}
|
|
@@ -174,10 +209,12 @@ var PiHarness = class extends LifecycleCapability {
|
|
|
174
209
|
async pending(options = {}) {
|
|
175
210
|
const { pi } = await _assertClassBrand(_PiHarness_brand, this, _open).call(this);
|
|
176
211
|
const id = options.session === void 0 ? void 0 : conversationId(options.session);
|
|
177
|
-
|
|
212
|
+
const inspection = await pi.inspect(BG);
|
|
213
|
+
const parked = await _assertClassBrand(_PiHarness_brand, this, _parked).call(this, inspection);
|
|
214
|
+
return inspection.submissions.filter((record) => record.requestId !== void 0 && (id === void 0 || record.conversationId === id)).map((record) => ({
|
|
178
215
|
operationId: record.requestId,
|
|
179
216
|
session: String(record.conversationId),
|
|
180
|
-
status: record.status === "queued" ? "queued" : "running"
|
|
217
|
+
status: record.status === "queued" ? "queued" : parked.some((task) => task.sessions.has(record.conversationId)) ? "parked" : "running"
|
|
181
218
|
}));
|
|
182
219
|
}
|
|
183
220
|
/** The opened pi Harness, for anything the interface does not cover. */
|
|
@@ -200,30 +237,33 @@ var PiHarness = class extends LifecycleCapability {
|
|
|
200
237
|
const operationId = options.operationId ?? crypto.randomUUID();
|
|
201
238
|
const { storage } = await _assertClassBrand(_PiHarness_brand, this, _open).call(this);
|
|
202
239
|
const conversation = await this.conversation(session);
|
|
203
|
-
_classPrivateFieldGet2(_admitting, this).set(session, (_classPrivateFieldGet2(_admitting, this).get(session) ?? 0) + 1);
|
|
204
|
-
let accepted;
|
|
205
|
-
try {
|
|
206
|
-
await _assertClassBrand(_PiHarness_brand, this, _wake).call(this, session);
|
|
207
|
-
accepted = await storage.submissionByRequest(conversation.id, operationId, BG) === void 0;
|
|
208
|
-
await conversation.submit({
|
|
209
|
-
type: "input",
|
|
210
|
-
content: input,
|
|
211
|
-
whenBusy: options.whenBusy ?? "followUp",
|
|
212
|
-
requestId: operationId,
|
|
213
|
-
...options.entry ? { entry: options.entry } : {}
|
|
214
|
-
}, BG);
|
|
215
|
-
} finally {
|
|
216
|
-
const left = (_classPrivateFieldGet2(_admitting, this).get(session) ?? 1) - 1;
|
|
217
|
-
if (left === 0) _classPrivateFieldGet2(_admitting, this).delete(session);
|
|
218
|
-
else _classPrivateFieldGet2(_admitting, this).set(session, left);
|
|
219
|
-
}
|
|
220
|
-
await _assertClassBrand(_PiHarness_brand, this, _wake).call(this, session);
|
|
221
240
|
return {
|
|
222
241
|
operationId,
|
|
223
242
|
session,
|
|
224
|
-
accepted
|
|
243
|
+
accepted: await _assertClassBrand(_PiHarness_brand, this, _admit).call(this, async () => {
|
|
244
|
+
const accepted = await storage.submissionByRequest(conversation.id, operationId, BG) === void 0;
|
|
245
|
+
await conversation.submit({
|
|
246
|
+
type: "input",
|
|
247
|
+
content: input,
|
|
248
|
+
whenBusy: options.whenBusy ?? "followUp",
|
|
249
|
+
requestId: operationId,
|
|
250
|
+
...options.entry ? { entry: options.entry } : {}
|
|
251
|
+
}, BG);
|
|
252
|
+
return accepted;
|
|
253
|
+
})
|
|
225
254
|
};
|
|
226
255
|
}
|
|
256
|
+
/** @internal Run again the tasks pi's restart limit parked in a session. */
|
|
257
|
+
async retryParked(session) {
|
|
258
|
+
const { pi } = await _assertClassBrand(_PiHarness_brand, this, _open).call(this);
|
|
259
|
+
const id = conversationId(session);
|
|
260
|
+
const parked = (await _assertClassBrand(_PiHarness_brand, this, _parked).call(this, await pi.inspect(BG))).filter((task) => task.sessions.has(id));
|
|
261
|
+
if (parked.length === 0) return false;
|
|
262
|
+
await _assertClassBrand(_PiHarness_brand, this, _admit).call(this, async () => {
|
|
263
|
+
for (const task of parked) await pi.retryTask(task.id, BG);
|
|
264
|
+
});
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
227
267
|
/** @internal Withdraw a queued input, or abort the run it joined. */
|
|
228
268
|
async withdraw(session, operationId) {
|
|
229
269
|
const { pi, storage } = await _assertClassBrand(_PiHarness_brand, this, _open).call(this);
|
|
@@ -256,94 +296,198 @@ var PiHarness = class extends LifecycleCapability {
|
|
|
256
296
|
};
|
|
257
297
|
}
|
|
258
298
|
};
|
|
259
|
-
/**
|
|
260
|
-
|
|
299
|
+
/**
|
|
300
|
+
* Hand pi work inside the wake. The wake is scheduled first, so if the
|
|
301
|
+
* object dies after pi has the work, that job restarts it; while `hand`
|
|
302
|
+
* is in flight the step will not complete. The wake steps again after, so
|
|
303
|
+
* it sees the work even if it just completed.
|
|
304
|
+
*/
|
|
305
|
+
async function _admit(hand) {
|
|
306
|
+
var _this$admitting;
|
|
307
|
+
_classPrivateFieldSet2(_admitting, this, (_this$admitting = _classPrivateFieldGet2(_admitting, this), _this$admitting++, _this$admitting));
|
|
308
|
+
let result;
|
|
309
|
+
try {
|
|
310
|
+
await _assertClassBrand(_PiHarness_brand, this, _wake).call(this);
|
|
311
|
+
result = await hand();
|
|
312
|
+
} finally {
|
|
313
|
+
var _this$admitting3;
|
|
314
|
+
_classPrivateFieldSet2(_admitting, this, (_this$admitting3 = _classPrivateFieldGet2(_admitting, this), _this$admitting3--, _this$admitting3));
|
|
315
|
+
}
|
|
316
|
+
await _assertClassBrand(_PiHarness_brand, this, _wake).call(this);
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
/** Schedule the wake job now, or at `time`. */
|
|
320
|
+
function _wake(time = Date.now()) {
|
|
261
321
|
return this.lifecycle.jobs.push({
|
|
262
|
-
id:
|
|
322
|
+
id: WAKE_JOB_ID,
|
|
263
323
|
fn: WAKE_FN,
|
|
264
324
|
time,
|
|
265
|
-
payload: { session },
|
|
266
325
|
singleflight: true,
|
|
267
326
|
recoveryLoop: true
|
|
268
327
|
});
|
|
269
328
|
}
|
|
329
|
+
/** Schedule the wake job for what `pi.wake()` says, if pi needs one. */
|
|
330
|
+
async function _wakeFor(wake) {
|
|
331
|
+
if (wake.active) await _assertClassBrand(_PiHarness_brand, this, _wake).call(this);
|
|
332
|
+
else if (wake.at !== void 0) await _assertClassBrand(_PiHarness_brand, this, _wake).call(this, wake.at);
|
|
333
|
+
}
|
|
270
334
|
/**
|
|
271
|
-
* One run of
|
|
272
|
-
*
|
|
273
|
-
*
|
|
335
|
+
* One run of the wake job. It never admits or replays anything: it reads
|
|
336
|
+
* `pi.wake()`, which is all of pi's sessions at once. Running work is
|
|
337
|
+
* waited on; a sleep reschedules the job for when it ends; no work
|
|
338
|
+
* completes the job. pi does all the work in between.
|
|
274
339
|
*/
|
|
275
|
-
async function _wakeStep(
|
|
340
|
+
async function _wakeStep() {
|
|
276
341
|
const heartbeat = { rescheduleAt: Date.now() + _classPrivateFieldGet2(_heartbeatMs, this) };
|
|
277
|
-
if (_classPrivateFieldGet2(
|
|
342
|
+
if (_classPrivateFieldGet2(_waiting, this)) return heartbeat;
|
|
278
343
|
const { pi } = await _assertClassBrand(_PiHarness_brand, this, _open).call(this);
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
344
|
+
pi.resume();
|
|
345
|
+
const wake = pi.wake();
|
|
346
|
+
if (wake.active) {
|
|
347
|
+
const wait = _assertClassBrand(_PiHarness_brand, this, _waitWhileActive).call(this, pi).then((ended) => {
|
|
348
|
+
_classPrivateFieldSet2(_waiting, this, void 0);
|
|
349
|
+
if (ended) _assertClassBrand(_PiHarness_brand, this, _wake).call(this);
|
|
350
|
+
}, (error) => {
|
|
351
|
+
_classPrivateFieldSet2(_waiting, this, void 0);
|
|
352
|
+
throw error;
|
|
353
|
+
});
|
|
354
|
+
_classPrivateFieldSet2(_waiting, this, wait);
|
|
355
|
+
if (!this.lifecycle.trackAlarmWork(wait)) wait.catch(() => {});
|
|
356
|
+
return heartbeat;
|
|
357
|
+
}
|
|
358
|
+
if (_classPrivateFieldGet2(_admitting, this) > 0) return heartbeat;
|
|
359
|
+
if (wake.at !== void 0) return { rescheduleAt: wake.at };
|
|
293
360
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
361
|
+
/**
|
|
362
|
+
* Wait while pi has work running, up to the wait budget. With `onWake`, a
|
|
363
|
+
* change ends each sleep early, and so does pi closing; without it, the
|
|
364
|
+
* sleep is `pollMs`. A task sleeping beside the running work, such as a
|
|
365
|
+
* retry backoff, also ends the sleep when it is due, and the wait calls
|
|
366
|
+
* `pi.resume()` to run it: with `onWake`, pi arms no timer for it, and
|
|
367
|
+
* the wake job's alarm is not set for it while the job waits. Whether the
|
|
368
|
+
* wait ended, with pi idle or at the budget, rather than failing. Rejects
|
|
369
|
+
* with a memory-limit reset.
|
|
370
|
+
*/
|
|
371
|
+
async function _waitWhileActive(pi) {
|
|
372
|
+
const deadline = Date.now() + _classPrivateFieldGet2(_waitBudgetMs, this);
|
|
297
373
|
try {
|
|
298
|
-
|
|
374
|
+
let wake = pi.wake();
|
|
375
|
+
while (wake.active) {
|
|
376
|
+
const now = Date.now();
|
|
377
|
+
const left = deadline - now;
|
|
378
|
+
if (left <= 0) return true;
|
|
379
|
+
let sleep = _classPrivateFieldGet2(_onWakeWired, this) ? left : Math.min(left, _classPrivateFieldGet2(_pollMs, this));
|
|
380
|
+
if (wake.at !== void 0) sleep = Math.min(sleep, wake.at - now);
|
|
381
|
+
await new Promise((resolve) => {
|
|
382
|
+
const timer = setTimeout(resolve, sleep);
|
|
383
|
+
_classPrivateFieldSet2(_changed, this, () => {
|
|
384
|
+
clearTimeout(timer);
|
|
385
|
+
resolve();
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
_classPrivateFieldSet2(_changed, this, void 0);
|
|
389
|
+
if (wake.at !== void 0 && Date.now() >= wake.at) pi.resume();
|
|
390
|
+
wake = pi.wake();
|
|
391
|
+
}
|
|
392
|
+
return true;
|
|
299
393
|
} catch (error) {
|
|
300
|
-
if (!
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
});
|
|
304
|
-
} finally {
|
|
305
|
-
clearTimeout(timer);
|
|
394
|
+
if (!_classPrivateFieldGet2(_closing, this).has(pi)) _assertClassBrand(_PiHarness_brand, this, _wakeError).call(this, error);
|
|
395
|
+
if (isDurableObjectMemoryLimitReset(error)) throw error;
|
|
396
|
+
return false;
|
|
306
397
|
}
|
|
307
398
|
}
|
|
399
|
+
function _wakeError(error) {
|
|
400
|
+
this.lifecycle.events.emit("pi:wake_error", { error: error instanceof Error ? error.message : String(error) });
|
|
401
|
+
}
|
|
308
402
|
/**
|
|
309
403
|
* pi, opened. Waits for Lifecycle startup first, so pi is only ever
|
|
310
404
|
* opened inside it: startup holds the input gate (`blockConcurrencyWhile`)
|
|
311
405
|
* and awaits the open in `onStart`, so an open begun before startup would
|
|
312
406
|
* be awaited behind a closed gate, with its timers and I/O held back, and
|
|
313
407
|
* never finish.
|
|
408
|
+
*
|
|
409
|
+
* pi stays open until `dispose()`, or until it closes on its own: any
|
|
410
|
+
* storage error fails pi's Session for good. Then the next call, or the
|
|
411
|
+
* wake job's next step, opens pi again once the failed pi has closed, and
|
|
412
|
+
* pi recovers from what it had committed. Closing waits for running task
|
|
413
|
+
* code to return, so code that ignores its abort signal delays the reopen.
|
|
414
|
+
* An open that fails is not kept either, so the next call tries again.
|
|
314
415
|
*/
|
|
315
416
|
async function _open() {
|
|
316
417
|
await this.lifecycle.ready();
|
|
317
|
-
_classPrivateFieldGet2(_opening, this)
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
418
|
+
if (_classPrivateFieldGet2(_opening, this) === void 0) {
|
|
419
|
+
const opening = _assertClassBrand(_PiHarness_brand, this, _doOpen).call(this, (pi, closed) => _assertClassBrand(_PiHarness_brand, this, _piClosing).call(this, opening, pi, closed)).catch((error) => {
|
|
420
|
+
if (_classPrivateFieldGet2(_opening, this) === opening) _classPrivateFieldSet2(_opening, this, void 0);
|
|
421
|
+
throw error;
|
|
422
|
+
});
|
|
423
|
+
_classPrivateFieldSet2(_opening, this, opening);
|
|
424
|
+
}
|
|
321
425
|
return _classPrivateFieldGet2(_opening, this);
|
|
322
426
|
}
|
|
323
|
-
async function _doOpen() {
|
|
427
|
+
async function _doOpen(onClose) {
|
|
428
|
+
await _classPrivateFieldGet2(_lastClose, this);
|
|
324
429
|
const storage = await openPiSessionStore(this.lifecycle.storage);
|
|
325
430
|
const pi = await _classPrivateFieldGet2(_options, this).harness({
|
|
326
431
|
storage,
|
|
327
|
-
context: BG
|
|
432
|
+
context: BG,
|
|
433
|
+
onWake: _classPrivateFieldGet2(_onWake, this)
|
|
328
434
|
});
|
|
329
|
-
|
|
330
|
-
|
|
435
|
+
try {
|
|
436
|
+
const closed = pi.closed;
|
|
437
|
+
pi.subscribeClose(() => onClose(pi, closed));
|
|
438
|
+
await pi.root(BG, { agent: this.agentDefaults() });
|
|
439
|
+
pi.resume();
|
|
440
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
441
|
+
if (!_classPrivateFieldGet2(_onWakeWired, this)) console.warn("PiHarness: the harness factory did not pass onWake to Harness.open, so pi's timers keep this Durable Object in memory while a task sleeps. Pass it: harness: ({ storage, context, onWake }) => Harness.open(storage, { ...options, onWake }, context)");
|
|
442
|
+
await _assertClassBrand(_PiHarness_brand, this, _wakeFor).call(this, pi.wake());
|
|
443
|
+
} catch (error) {
|
|
444
|
+
pi.close(BG).catch(() => {});
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
331
447
|
return {
|
|
332
448
|
pi,
|
|
333
449
|
storage
|
|
334
450
|
};
|
|
335
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* pi began closing: on its own, as a storage error makes it, or by
|
|
454
|
+
* `dispose()`.
|
|
455
|
+
*/
|
|
456
|
+
function _piClosing(opening, pi, closed) {
|
|
457
|
+
_classPrivateFieldGet2(_closing, this).add(pi);
|
|
458
|
+
_classPrivateFieldGet2(_changed, this)?.call(this);
|
|
459
|
+
if (_classPrivateFieldGet2(_opening, this) !== opening) return;
|
|
460
|
+
_classPrivateFieldSet2(_opening, this, void 0);
|
|
461
|
+
_classPrivateFieldSet2(_lastClose, this, closed);
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Tasks pi's restart limit parked, each with the sessions whose work it
|
|
465
|
+
* is: its own, and every session that owns that one, such as the session
|
|
466
|
+
* of a tool that started a subagent.
|
|
467
|
+
*/
|
|
468
|
+
async function _parked(inspection) {
|
|
469
|
+
const { storage } = await _assertClassBrand(_PiHarness_brand, this, _open).call(this);
|
|
470
|
+
const parked = [];
|
|
471
|
+
for (const { record, state } of inspection.tasks) {
|
|
472
|
+
if (state.kind !== "blocked" || state.reason !== "restart_limit") continue;
|
|
473
|
+
const sessions = /* @__PURE__ */ new Set();
|
|
474
|
+
let id = record.conversationId;
|
|
475
|
+
while (id !== void 0 && !sessions.has(id)) {
|
|
476
|
+
sessions.add(id);
|
|
477
|
+
id = (await storage.conversation(id, BG))?.owner?.conversationId;
|
|
478
|
+
}
|
|
479
|
+
parked.push({
|
|
480
|
+
id: record.id,
|
|
481
|
+
sessions
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
return parked;
|
|
485
|
+
}
|
|
336
486
|
async function _findSubmission(id, operationId, context) {
|
|
337
487
|
const { pi, storage } = await _assertClassBrand(_PiHarness_brand, this, _open).call(this);
|
|
338
488
|
const record = await storage.submissionByRequest(id, operationId, context);
|
|
339
489
|
return record ? pi.submission(record.id, context) : void 0;
|
|
340
490
|
}
|
|
341
|
-
/** When pi's current wait is far enough away to hand to the alarm. */
|
|
342
|
-
async function _longWait(pi, id, context) {
|
|
343
|
-
const live = await pi.snapshot(LiveDoc, id, context);
|
|
344
|
-
const at = live?.generation?.deferred?.pollAt ?? live?.generation?.retry?.at;
|
|
345
|
-
return at !== void 0 && at - Date.now() > _classPrivateFieldGet2(_sleepThresholdMs, this) ? at : void 0;
|
|
346
|
-
}
|
|
347
491
|
async function _result(session, operationId, settled, context) {
|
|
348
492
|
if (settled.status === "unanswered") return {
|
|
349
493
|
operationId,
|
|
@@ -407,6 +551,14 @@ var PiSession = class {
|
|
|
407
551
|
await (await _classPrivateFieldGet2(_harness, this).conversation(this.id)).abort(BG);
|
|
408
552
|
return true;
|
|
409
553
|
}
|
|
554
|
+
/**
|
|
555
|
+
* Run again the tasks pi's crash-loop guard (`settings.restartLimit`)
|
|
556
|
+
* parked in this session's work, including subagent sessions it owns.
|
|
557
|
+
* False when nothing was parked.
|
|
558
|
+
*/
|
|
559
|
+
retry() {
|
|
560
|
+
return _classPrivateFieldGet2(_harness, this).retryParked(this.id);
|
|
561
|
+
}
|
|
410
562
|
/** Start a new context, optionally from a handoff note. */
|
|
411
563
|
async reset(handoff) {
|
|
412
564
|
await (await _classPrivateFieldGet2(_harness, this).conversation(this.id)).reset(handoff, BG);
|