@canonmsg/agent-sdk 13.0.3 → 13.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 +10 -0
- package/dist/chunks/{chunk-XKUG2BQR.js → chunk-IEKVS7AN.js} +272 -75
- package/dist/cli.js +49 -5
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -1
- package/dist/internal/endpoint/transient-publisher.d.ts +3 -2
- package/dist/runner/runner.d.ts +28 -0
- package/dist/runner/wake.d.ts +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -227,6 +227,8 @@ Every room is encrypted, so the runner needs the agent's MLS device, which it ke
|
|
|
227
227
|
| `agentTurnCap` | 6 | Encrypted rooms: how many agent @mentions in a row wake the agent in a room before a person writes there again |
|
|
228
228
|
| `mentionAliases` | none | Encrypted rooms: other names that @mention the agent, such as its role |
|
|
229
229
|
| `sessionApprovalForDestructive` | off | Encrypted rooms: whether the owner may allow an action the runtime marks destructive for the rest of the session (CLI `--allow-session-for-destructive`) |
|
|
230
|
+
| `wake` | left as registered | The agent's signed wake callback, registered at each start, or `null` to remove it; see [Waking and sleeping](#waking-and-sleeping) |
|
|
231
|
+
| `idleTimeoutMs`, `onIdle` | off | `onIdle(activity)` once the runner has had nothing to do for this long; see [Waking and sleeping](#waking-and-sleeping) |
|
|
230
232
|
|
|
231
233
|
`baseUrl`, `streamUrl`, `endpoint` and `fetch` are test seams; production never sets them. `codeDriver`'s second argument overrides its capabilities, such as `outboundFiles` and `maxConcurrentTurns` (default 8).
|
|
232
234
|
|
|
@@ -328,6 +330,14 @@ canon-agent errands --profile engineer --cancel <errand id>
|
|
|
328
330
|
- **Replies.** Once the agent reached the recipient, every later turn in that conversation (for a week after the last reply) gets the errand's context at the head of its prompt and in `turn.input.errands`, so the agent knows why it is there. Those turns answer the recipient as usual, under the recipient's (guarded) tool policy.
|
|
329
331
|
- **Status.** `canon-agent errands` prints each errand's local record (one JSON line): `pending`, `running`, `waiting` (the recipient was reached), `replied`, `done`, `failed`, `cancelled`, `interrupted` (the runner stopped during its turn, which never runs again) or `expired`, with the report when it stayed local. `--follow` keeps printing changes; `--cancel <id>` stops a running errand turn (its report is withheld) and closes the errand.
|
|
330
332
|
|
|
333
|
+
## Waking and sleeping
|
|
334
|
+
|
|
335
|
+
An agent need not run all the time. Canon can wake it, and the runner says when it has nothing to do, so its host can stop or suspend it in between ([agent-wake.md](https://github.com/HeyBobChan/canon/blob/main/docs/agent-wake.md)).
|
|
336
|
+
|
|
337
|
+
- **Wake.** `wake: { callbackUrl, secret, conversationIds }` (CLI `--wake-config <file>`) registers, at each start, a public HTTPS callback for up to 32 rooms. When a person types in one, or a message arrives there, Canon POSTs it a signed [Standard Webhooks](https://www.standardwebhooks.com/) request whose body is only `{"type":"wake","reason":"typing"|"event"}`: no room, sender or content. The host starts or resumes the agent and answers 2xx once it is ready; the agent then reads its input as usual. The secret is `whsec_` and the padded base64 of 32 random bytes (`echo "whsec_$(openssl rand -base64 32)"`). Canon first sends the URL a signed `{"type":"verification","challenge":"…"}`, which must be answered with `{"challenge":"…"}` and wake nothing, and the start fails if it is not. `wake: null` (`--wake-off`) removes the callback; leaving it out leaves it as it is.
|
|
338
|
+
- **Idle.** `runner.activity()` reports what the runner has in hand, as counts: running `turns`, `inputs` waiting for one, `errands` waiting, and `outbound` operations Canon has not answered yet (a reply waiting out an outage counts). It is `idle` when all are 0. With `idleTimeoutMs` (at least 1 s), `onIdle(activity)` is called once the runner has been idle that long, and again only after new activity. The CLI's `--idle-timeout 10m` stops gracefully and exits 0 then.
|
|
339
|
+
- **What a host must add.** Typing reaches the host's callback, not the runner, so a host keeps the agent awake while a person types (Canon sends at most one typing hint per room every 10 s while they do). A wake that arrives just as the runner goes idle can leave its input in the inbox: a host that has received a wake since the runner last reported idle starts it again. Errands written while the agent is stopped run at its next start.
|
|
340
|
+
|
|
331
341
|
## Registration
|
|
332
342
|
|
|
333
343
|
`ensureAgentProfile` registers an agent and keeps its key in `$CANON_HOME/agents.json` (default `~/.canon`), as a profile the CLI can use with `--profile`. It reuses an environment-bound profile, resumes the exact saved approval request, persists credentials before acknowledging delivery, and retries an interrupted acknowledgement on the next call. `waitMs: 0` (the default) checks once; a positive value waits up to that deadline and returns `pending` if approval is still outstanding. See the [complete runnable quickstart](https://canonmail.com/agents/build#run-a-complete-agent).
|
|
@@ -262,9 +262,9 @@ function stdioRpc(child, handlers, { name, envelope }) {
|
|
|
262
262
|
}, PIPE_GRACE_MS).unref();
|
|
263
263
|
});
|
|
264
264
|
return {
|
|
265
|
-
call: /* @__PURE__ */ __name((method, params) => new Promise((
|
|
265
|
+
call: /* @__PURE__ */ __name((method, params) => new Promise((resolve4, reject) => {
|
|
266
266
|
next += 1;
|
|
267
|
-
pending.set(next, { resolve: /* @__PURE__ */ __name((value) =>
|
|
267
|
+
pending.set(next, { resolve: /* @__PURE__ */ __name((value) => resolve4(isRecord(value) ? value : {}), "resolve"), reject });
|
|
268
268
|
write({ id: next, method, params });
|
|
269
269
|
}), "call"),
|
|
270
270
|
notify: /* @__PURE__ */ __name((method, params) => write({ method, ...params ? { params } : {} }), "notify"),
|
|
@@ -341,11 +341,11 @@ async function checkAcp({ command, args = [], cwd, timeoutMs = 3e4 }) {
|
|
|
341
341
|
const link = stdioRpc(child, { notify: /* @__PURE__ */ __name(() => void 0, "notify"), request: /* @__PURE__ */ __name((method) => refuseMethod(method), "request") }, { name: basename(command), envelope: { jsonrpc: "2.0" } });
|
|
342
342
|
let timer;
|
|
343
343
|
try {
|
|
344
|
-
return await new Promise((
|
|
344
|
+
return await new Promise((resolve4, reject) => {
|
|
345
345
|
timer = setTimeout(() => reject(new Error(`${command} did not answer the Agent Client Protocol's initialize within ${timeoutMs / 1e3} s`)), timeoutMs);
|
|
346
346
|
child.once("error", (error) => reject(new Error(`Could not run ${command}: ${error.message}`)));
|
|
347
347
|
child.once("close", () => reject(new Error(`${command} exited before answering the Agent Client Protocol's initialize`)));
|
|
348
|
-
initialize(link, command).then(({ capabilities, info }) =>
|
|
348
|
+
initialize(link, command).then(({ capabilities, info }) => resolve4({
|
|
349
349
|
name: text(info.title) || text(info.name) || basename(command),
|
|
350
350
|
version: text(info.version),
|
|
351
351
|
sessionMemory: restoreMethod(capabilities) ? "native" : "none"
|
|
@@ -401,8 +401,8 @@ function acpDriver(options) {
|
|
|
401
401
|
if (guest && options.guestMode === void 0 && !options.unguardedGuests) return { reply: OWNER_ONLY };
|
|
402
402
|
const child = startAgent(command, args, cwd);
|
|
403
403
|
let exit = /* @__PURE__ */ __name(() => void 0, "exit");
|
|
404
|
-
const exited = new Promise((
|
|
405
|
-
exit =
|
|
404
|
+
const exited = new Promise((resolve4) => {
|
|
405
|
+
exit = resolve4;
|
|
406
406
|
});
|
|
407
407
|
let killing;
|
|
408
408
|
let stopping = false;
|
|
@@ -413,7 +413,7 @@ function acpDriver(options) {
|
|
|
413
413
|
}, "close"), exited };
|
|
414
414
|
running.set(turn.session, entry);
|
|
415
415
|
const server = `${CANON}_${randomBytes(4).toString("hex")}`;
|
|
416
|
-
return new Promise((
|
|
416
|
+
return new Promise((resolve4, reject) => {
|
|
417
417
|
const messages = [];
|
|
418
418
|
const calls = /* @__PURE__ */ new Map();
|
|
419
419
|
let sessionId = "";
|
|
@@ -439,7 +439,7 @@ function acpDriver(options) {
|
|
|
439
439
|
finish();
|
|
440
440
|
if (turn.signal.aborted) reject(turn.signal.reason);
|
|
441
441
|
else if (outcome instanceof Error) reject(outcome);
|
|
442
|
-
else
|
|
442
|
+
else resolve4(outcome);
|
|
443
443
|
}, "settle");
|
|
444
444
|
const stopGuest = /* @__PURE__ */ __name((what) => {
|
|
445
445
|
link.notify("session/cancel", { sessionId });
|
|
@@ -532,10 +532,10 @@ ${text(item.newText)}`);
|
|
|
532
532
|
child.once("exit", () => {
|
|
533
533
|
if (stopping || guest || turn.signal.aborted) {
|
|
534
534
|
signalAgent(child, "SIGTERM");
|
|
535
|
-
ended = new Promise((
|
|
535
|
+
ended = new Promise((resolve5) => {
|
|
536
536
|
setTimeout(() => {
|
|
537
537
|
signalAgent(child, "SIGKILL");
|
|
538
|
-
|
|
538
|
+
resolve5();
|
|
539
539
|
}, KILL_AFTER_MS);
|
|
540
540
|
});
|
|
541
541
|
}
|
|
@@ -789,8 +789,8 @@ function claudeDriver(options) {
|
|
|
789
789
|
const resume = turn.state.get(SESSION2);
|
|
790
790
|
const native = query({ prompt: turn.prompt, options: queryOptions(turn, resume) });
|
|
791
791
|
let exit = /* @__PURE__ */ __name(() => void 0, "exit");
|
|
792
|
-
const entry = { close: /* @__PURE__ */ __name(() => native.close(), "close"), exited: new Promise((
|
|
793
|
-
exit =
|
|
792
|
+
const entry = { close: /* @__PURE__ */ __name(() => native.close(), "close"), exited: new Promise((resolve4) => {
|
|
793
|
+
exit = resolve4;
|
|
794
794
|
}) };
|
|
795
795
|
running.set(turn.session, entry);
|
|
796
796
|
let closing;
|
|
@@ -803,7 +803,7 @@ function claudeDriver(options) {
|
|
|
803
803
|
}, "interrupt");
|
|
804
804
|
turn.signal.addEventListener("abort", interrupt, { once: true });
|
|
805
805
|
const reader = liveReader(turn);
|
|
806
|
-
return new Promise((
|
|
806
|
+
return new Promise((resolve4, reject) => void (async () => {
|
|
807
807
|
let session = resume;
|
|
808
808
|
let started = false;
|
|
809
809
|
let finished = false;
|
|
@@ -820,7 +820,7 @@ function claudeDriver(options) {
|
|
|
820
820
|
closeLater();
|
|
821
821
|
if (message2.subtype === "success" && !message2.is_error) {
|
|
822
822
|
const reply = message2.result.trim() ? message2.result : reader.text();
|
|
823
|
-
|
|
823
|
+
resolve4({ reply: reply.trim() ? reply : null });
|
|
824
824
|
} else {
|
|
825
825
|
const detail = message2.subtype === "success" ? message2.result : message2.errors.join("; ");
|
|
826
826
|
reject(failed(new Error(`Claude Code ended the turn with ${message2.subtype}${detail ? `: ${oneLine2(detail, 300)}` : ""}`)));
|
|
@@ -1112,14 +1112,14 @@ function codexDriver(options) {
|
|
|
1112
1112
|
const cli = installedCli(codexPath);
|
|
1113
1113
|
const child = spawn3(cli.file, [...cli.args, "app-server", "--listen", "stdio://"], { cwd, env, stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
|
|
1114
1114
|
let exit = /* @__PURE__ */ __name(() => void 0, "exit");
|
|
1115
|
-
const exited = new Promise((
|
|
1116
|
-
exit =
|
|
1115
|
+
const exited = new Promise((resolve4) => {
|
|
1116
|
+
exit = resolve4;
|
|
1117
1117
|
});
|
|
1118
1118
|
const entry = { close: /* @__PURE__ */ __name(() => {
|
|
1119
1119
|
stopCli(child);
|
|
1120
1120
|
}, "close"), exited };
|
|
1121
1121
|
running.set(turn.session, entry);
|
|
1122
|
-
return new Promise((
|
|
1122
|
+
return new Promise((resolve4, reject) => {
|
|
1123
1123
|
const messages = /* @__PURE__ */ new Map();
|
|
1124
1124
|
const items = /* @__PURE__ */ new Map();
|
|
1125
1125
|
const completed = /* @__PURE__ */ new Map();
|
|
@@ -1145,7 +1145,7 @@ function codexDriver(options) {
|
|
|
1145
1145
|
finish();
|
|
1146
1146
|
if (turn.signal.aborted) reject(turn.signal.reason);
|
|
1147
1147
|
else if (outcome instanceof Error) reject(outcome);
|
|
1148
|
-
else
|
|
1148
|
+
else resolve4(outcome);
|
|
1149
1149
|
}, "settle");
|
|
1150
1150
|
const conclude = /* @__PURE__ */ __name(() => {
|
|
1151
1151
|
const ended = completed.get(nativeTurn);
|
|
@@ -1343,8 +1343,8 @@ function connectorDriver() {
|
|
|
1343
1343
|
entry.confirmation = confirmation;
|
|
1344
1344
|
const deadline = options.deadline ?? Date.now() + CONNECTOR_RELAY_ANSWER_WITHIN_MS;
|
|
1345
1345
|
let timer;
|
|
1346
|
-
const late = new Promise((
|
|
1347
|
-
timer = setTimeout(
|
|
1346
|
+
const late = new Promise((resolve4) => {
|
|
1347
|
+
timer = setTimeout(resolve4, Math.max(0, deadline - Date.now()), "late");
|
|
1348
1348
|
});
|
|
1349
1349
|
const confirmed = await Promise.race([confirmation, late]).finally(() => clearTimeout(timer));
|
|
1350
1350
|
const state = entry.state;
|
|
@@ -1383,7 +1383,7 @@ function connectorDriver() {
|
|
|
1383
1383
|
])).digest("hex").slice(0, 40)}`;
|
|
1384
1384
|
const input = fitInput(inputId, turn);
|
|
1385
1385
|
if (!input) return Promise.reject(new Error(TOO_LARGE));
|
|
1386
|
-
return new Promise((
|
|
1386
|
+
return new Promise((resolve4, reject) => {
|
|
1387
1387
|
const entry = {
|
|
1388
1388
|
ref: { inputId, conversationId: turn.input.conversationId, messageId: messages.at(-1).id, createdAt: messages.at(-1).createdAt },
|
|
1389
1389
|
session: turn.session,
|
|
@@ -1393,9 +1393,9 @@ function connectorDriver() {
|
|
|
1393
1393
|
finish: /* @__PURE__ */ __name((result) => {
|
|
1394
1394
|
turn.signal.removeEventListener("abort", onAbort);
|
|
1395
1395
|
if (result instanceof Error) reject(result);
|
|
1396
|
-
else if (result.outcome === "reply")
|
|
1396
|
+
else if (result.outcome === "reply") resolve4({ reply: result.text });
|
|
1397
1397
|
else if (result.outcome === "failed") reject(new Error(FAILED));
|
|
1398
|
-
else
|
|
1398
|
+
else resolve4({ reply: null });
|
|
1399
1399
|
}, "finish")
|
|
1400
1400
|
};
|
|
1401
1401
|
const onAbort = /* @__PURE__ */ __name(() => withdraw(entry, turn.signal.reason), "onAbort");
|
|
@@ -1531,7 +1531,7 @@ var answered = /* @__PURE__ */ __name((kind, record) => record.status !== "sendi
|
|
|
1531
1531
|
async function awaitErrand(directory, command, ms) {
|
|
1532
1532
|
const journal = errandJournal(directory);
|
|
1533
1533
|
let record;
|
|
1534
|
-
for (const deadline = Date.now() + ms; Date.now() < deadline; await new Promise((
|
|
1534
|
+
for (const deadline = Date.now() + ms; Date.now() < deadline; await new Promise((resolve4) => setTimeout(resolve4, 200))) {
|
|
1535
1535
|
record = journal.get(command.id);
|
|
1536
1536
|
if (record && answered(command.kind, record)) break;
|
|
1537
1537
|
}
|
|
@@ -2161,9 +2161,9 @@ function managedMlsDevice(options) {
|
|
|
2161
2161
|
}
|
|
2162
2162
|
__name(device, "device");
|
|
2163
2163
|
const custody2 = {
|
|
2164
|
-
prepareProtected: /* @__PURE__ */ __name((context, ...rest) => device(context, false).then((
|
|
2165
|
-
prepareProtectedKeyPackage: /* @__PURE__ */ __name((context, ...rest) => device(context, false).then((
|
|
2166
|
-
prepareProtectedGroup: /* @__PURE__ */ __name((identity, ...rest) => device(identity, false).then((
|
|
2164
|
+
prepareProtected: /* @__PURE__ */ __name((context, ...rest) => device(context, false).then((open5) => open5.prepareProtected(context, ...rest)), "prepareProtected"),
|
|
2165
|
+
prepareProtectedKeyPackage: /* @__PURE__ */ __name((context, ...rest) => device(context, false).then((open5) => open5.prepareProtectedKeyPackage(context, ...rest)), "prepareProtectedKeyPackage"),
|
|
2166
|
+
prepareProtectedGroup: /* @__PURE__ */ __name((identity, ...rest) => device(identity, false).then((open5) => open5.prepareProtectedGroup(identity, ...rest)), "prepareProtectedGroup")
|
|
2167
2167
|
};
|
|
2168
2168
|
return {
|
|
2169
2169
|
configuration,
|
|
@@ -2181,7 +2181,7 @@ function managedMlsDevice(options) {
|
|
|
2181
2181
|
return false;
|
|
2182
2182
|
},
|
|
2183
2183
|
async close() {
|
|
2184
|
-
await Promise.allSettled([...devices.values()].map((
|
|
2184
|
+
await Promise.allSettled([...devices.values()].map((open5) => open5.then((custody3) => custody3.close())));
|
|
2185
2185
|
}
|
|
2186
2186
|
};
|
|
2187
2187
|
}
|
|
@@ -2685,15 +2685,15 @@ import { createHash as createHash7 } from "node:crypto";
|
|
|
2685
2685
|
import { readdirSync as readdirSync3, readFileSync as readFileSync5, rmSync } from "node:fs";
|
|
2686
2686
|
import { join as join6 } from "node:path";
|
|
2687
2687
|
var sha2562 = /* @__PURE__ */ __name((value) => createHash7("sha256").update(value).digest("hex"), "sha256");
|
|
2688
|
-
var pause = /* @__PURE__ */ __name((ms) => new Promise((
|
|
2689
|
-
setTimeout(
|
|
2688
|
+
var pause = /* @__PURE__ */ __name((ms) => new Promise((resolve4) => {
|
|
2689
|
+
setTimeout(resolve4, ms).unref?.();
|
|
2690
2690
|
}), "pause");
|
|
2691
|
-
var pauseUnless = /* @__PURE__ */ __name((ms, signal) => new Promise((
|
|
2692
|
-
if (signal.aborted) return
|
|
2691
|
+
var pauseUnless = /* @__PURE__ */ __name((ms, signal) => new Promise((resolve4) => {
|
|
2692
|
+
if (signal.aborted) return resolve4();
|
|
2693
2693
|
const done = /* @__PURE__ */ __name(() => {
|
|
2694
2694
|
clearTimeout(timer);
|
|
2695
2695
|
signal.removeEventListener("abort", done);
|
|
2696
|
-
|
|
2696
|
+
resolve4();
|
|
2697
2697
|
}, "done");
|
|
2698
2698
|
const timer = setTimeout(done, ms);
|
|
2699
2699
|
timer.unref?.();
|
|
@@ -2986,14 +2986,14 @@ function createMlsAsks(options, approvalScope2) {
|
|
|
2986
2986
|
}
|
|
2987
2987
|
}
|
|
2988
2988
|
if (signal.aborted) throw ended();
|
|
2989
|
-
await new Promise((
|
|
2989
|
+
await new Promise((resolve4) => {
|
|
2990
2990
|
const timer = setTimeout(done, Math.max(0, Math.min(pollMs, expiresAtMs - Date.now())));
|
|
2991
2991
|
timer.unref?.();
|
|
2992
2992
|
function done() {
|
|
2993
2993
|
clearTimeout(timer);
|
|
2994
2994
|
signal.removeEventListener("abort", done);
|
|
2995
2995
|
card.wake = () => void 0;
|
|
2996
|
-
|
|
2996
|
+
resolve4();
|
|
2997
2997
|
}
|
|
2998
2998
|
__name(done, "done");
|
|
2999
2999
|
card.wake = done;
|
|
@@ -3004,12 +3004,12 @@ function createMlsAsks(options, approvalScope2) {
|
|
|
3004
3004
|
__name(wait, "wait");
|
|
3005
3005
|
function bounded(card, work) {
|
|
3006
3006
|
const { expiresAtMs, signal } = card.input;
|
|
3007
|
-
return new Promise((
|
|
3008
|
-
const stop = /* @__PURE__ */ __name(() =>
|
|
3007
|
+
return new Promise((resolve4, reject) => {
|
|
3008
|
+
const stop = /* @__PURE__ */ __name(() => resolve4(void 0), "stop");
|
|
3009
3009
|
const timer = setTimeout(stop, Math.max(0, expiresAtMs - Date.now()));
|
|
3010
3010
|
timer.unref?.();
|
|
3011
3011
|
signal.addEventListener("abort", stop, { once: true });
|
|
3012
|
-
work.then(
|
|
3012
|
+
work.then(resolve4, reject).finally(() => {
|
|
3013
3013
|
clearTimeout(timer);
|
|
3014
3014
|
signal.removeEventListener("abort", stop);
|
|
3015
3015
|
});
|
|
@@ -3058,11 +3058,11 @@ function createMlsAsks(options, approvalScope2) {
|
|
|
3058
3058
|
throw error;
|
|
3059
3059
|
});
|
|
3060
3060
|
})();
|
|
3061
|
-
return new Promise((
|
|
3061
|
+
return new Promise((resolve4, reject) => {
|
|
3062
3062
|
const stop = /* @__PURE__ */ __name(() => reject(turnEnded()), "stop");
|
|
3063
3063
|
if (signal.aborted) stop();
|
|
3064
3064
|
else signal.addEventListener("abort", stop, { once: true });
|
|
3065
|
-
work.then(
|
|
3065
|
+
work.then(resolve4, reject).finally(() => signal.removeEventListener("abort", stop));
|
|
3066
3066
|
});
|
|
3067
3067
|
},
|
|
3068
3068
|
wake,
|
|
@@ -3158,9 +3158,9 @@ async function startToolServer() {
|
|
|
3158
3158
|
if (!response.headersSent) response.writeHead(500).end();
|
|
3159
3159
|
});
|
|
3160
3160
|
});
|
|
3161
|
-
await new Promise((
|
|
3161
|
+
await new Promise((resolve4, reject) => {
|
|
3162
3162
|
http.once("error", reject);
|
|
3163
|
-
http.listen(0, "127.0.0.1",
|
|
3163
|
+
http.listen(0, "127.0.0.1", resolve4);
|
|
3164
3164
|
});
|
|
3165
3165
|
const { port } = http.address();
|
|
3166
3166
|
return {
|
|
@@ -3171,10 +3171,10 @@ async function startToolServer() {
|
|
|
3171
3171
|
turns.delete(token);
|
|
3172
3172
|
}, "release") };
|
|
3173
3173
|
},
|
|
3174
|
-
close: /* @__PURE__ */ __name(() => new Promise((
|
|
3174
|
+
close: /* @__PURE__ */ __name(() => new Promise((resolve4) => {
|
|
3175
3175
|
turns.clear();
|
|
3176
3176
|
http.closeAllConnections();
|
|
3177
|
-
http.close(() =>
|
|
3177
|
+
http.close(() => resolve4());
|
|
3178
3178
|
}), "close")
|
|
3179
3179
|
};
|
|
3180
3180
|
}
|
|
@@ -3183,6 +3183,9 @@ __name(startToolServer, "startToolServer");
|
|
|
3183
3183
|
// src/runner/runner.ts
|
|
3184
3184
|
var STOPS = ["stop", "drop", "new_session", "membership", "audience"];
|
|
3185
3185
|
var MAX_INPUTS_PER_TURN = 20;
|
|
3186
|
+
var OUTBOUND_UNANSWERED = ["queued", "sending", "deferred", "pending"];
|
|
3187
|
+
var INBOUND_UNSETTLED = ["observed", "deferred", "offered"];
|
|
3188
|
+
var ACTIVITY_COUNT_LIMIT = 1e3;
|
|
3186
3189
|
var CLAIM_RETRY_MS = 250;
|
|
3187
3190
|
var CLAIM_PATIENCE_MS = 2e4;
|
|
3188
3191
|
var HISTORY_WINDOW = 20;
|
|
@@ -3248,14 +3251,14 @@ var toolText = /* @__PURE__ */ __name((text3, isError = false) => ({ content: [{
|
|
|
3248
3251
|
var turnEnded2 = /* @__PURE__ */ __name(() => new DOMException("This turn has ended", "AbortError"), "turnEnded");
|
|
3249
3252
|
function within(promise, ms) {
|
|
3250
3253
|
let timer;
|
|
3251
|
-
const expiry = new Promise((
|
|
3252
|
-
timer = setTimeout(
|
|
3254
|
+
const expiry = new Promise((resolve4) => {
|
|
3255
|
+
timer = setTimeout(resolve4, ms, void 0);
|
|
3253
3256
|
});
|
|
3254
3257
|
return Promise.race([promise, expiry]).finally(() => clearTimeout(timer));
|
|
3255
3258
|
}
|
|
3256
3259
|
__name(within, "within");
|
|
3257
|
-
var pause2 = /* @__PURE__ */ __name((ms) => new Promise((
|
|
3258
|
-
setTimeout(
|
|
3260
|
+
var pause2 = /* @__PURE__ */ __name((ms) => new Promise((resolve4) => {
|
|
3261
|
+
setTimeout(resolve4, ms).unref?.();
|
|
3259
3262
|
}), "pause");
|
|
3260
3263
|
function agentRun(turns) {
|
|
3261
3264
|
const history = turns.map(({ senderId, senderType }) => senderType === "ai_agent" ? { senderId, senderType, metadata: { turnSemantics: "turn_complete" } } : { senderId, senderType: "human" });
|
|
@@ -3432,6 +3435,11 @@ async function createRunner(options) {
|
|
|
3432
3435
|
const maxTurns = driver.capabilities.maxConcurrentTurns ?? 8;
|
|
3433
3436
|
const agentTurnCap = options.agentTurnCap ?? 6;
|
|
3434
3437
|
if (!Number.isSafeInteger(agentTurnCap) || agentTurnCap < 0 || agentTurnCap > 1e3) throw new Error("agentTurnCap must be a whole number from 0 to 1000");
|
|
3438
|
+
const { idleTimeoutMs, onIdle } = options;
|
|
3439
|
+
if (idleTimeoutMs === void 0 !== (onIdle === void 0)) throw new Error("idleTimeoutMs and onIdle go together");
|
|
3440
|
+
if (idleTimeoutMs !== void 0 && (!Number.isSafeInteger(idleTimeoutMs) || idleTimeoutMs < 1e3)) {
|
|
3441
|
+
throw new Error("idleTimeoutMs must be a whole number of milliseconds, at least 1000");
|
|
3442
|
+
}
|
|
3435
3443
|
const names = /* @__PURE__ */ __name((list, what) => {
|
|
3436
3444
|
if (list !== void 0 && (!Array.isArray(list) || list.some((name) => typeof name !== "string" || !name.trim() || name.length > 200))) {
|
|
3437
3445
|
throw new Error(`${what} must be a list of non-empty names`);
|
|
@@ -3484,13 +3492,90 @@ async function createRunner(options) {
|
|
|
3484
3492
|
const halt = new AbortController();
|
|
3485
3493
|
const untilHalt = /* @__PURE__ */ __name((work) => {
|
|
3486
3494
|
let stop = /* @__PURE__ */ __name(() => void 0, "stop");
|
|
3487
|
-
const halted = new Promise((
|
|
3488
|
-
stop = /* @__PURE__ */ __name(() =>
|
|
3495
|
+
const halted = new Promise((resolve4) => {
|
|
3496
|
+
stop = /* @__PURE__ */ __name(() => resolve4("pending"), "stop");
|
|
3489
3497
|
if (halt.signal.aborted) stop();
|
|
3490
3498
|
else halt.signal.addEventListener("abort", stop, { once: true });
|
|
3491
3499
|
});
|
|
3492
3500
|
return Promise.race([work, halted]).finally(() => halt.signal.removeEventListener("abort", stop));
|
|
3493
3501
|
}, "untilHalt");
|
|
3502
|
+
let lastActiveAt = Date.now();
|
|
3503
|
+
let idleCalled = false;
|
|
3504
|
+
let offering = 0;
|
|
3505
|
+
let calls = 0;
|
|
3506
|
+
const touch = /* @__PURE__ */ __name(() => {
|
|
3507
|
+
lastActiveAt = Date.now();
|
|
3508
|
+
idleCalled = false;
|
|
3509
|
+
}, "touch");
|
|
3510
|
+
async function activeCall(work) {
|
|
3511
|
+
calls += 1;
|
|
3512
|
+
touch();
|
|
3513
|
+
try {
|
|
3514
|
+
return await work();
|
|
3515
|
+
} finally {
|
|
3516
|
+
calls -= 1;
|
|
3517
|
+
touch();
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
__name(activeCall, "activeCall");
|
|
3521
|
+
function ownActivity() {
|
|
3522
|
+
let turns = running.size;
|
|
3523
|
+
let queued2 = offering;
|
|
3524
|
+
for (const current of rooms.values()) {
|
|
3525
|
+
queued2 += current.queue.length;
|
|
3526
|
+
if (current.recovering) turns += 1;
|
|
3527
|
+
if (current.resetting) turns += 1;
|
|
3528
|
+
}
|
|
3529
|
+
let errandsWaiting = 0;
|
|
3530
|
+
for (const record of errandsById.values()) if (record.status === "pending") errandsWaiting += 1;
|
|
3531
|
+
return { turns, queued: queued2, errands: errandsWaiting, sending: posting.size + calls };
|
|
3532
|
+
}
|
|
3533
|
+
__name(ownActivity, "ownActivity");
|
|
3534
|
+
async function activity() {
|
|
3535
|
+
if (stopping) throw new Error("The runner is stopping");
|
|
3536
|
+
if (!inbound) throw new Error("Start the runner first");
|
|
3537
|
+
const [operations, records] = await Promise.all([
|
|
3538
|
+
endpoint.listOperations({ statuses: [...OUTBOUND_UNANSWERED], limit: ACTIVITY_COUNT_LIMIT }),
|
|
3539
|
+
endpoint.listInbound({ states: [...INBOUND_UNSETTLED], limit: ACTIVITY_COUNT_LIMIT })
|
|
3540
|
+
]);
|
|
3541
|
+
const own = ownActivity();
|
|
3542
|
+
const unsettled = records.filter(({ event, deleted, externalClaim }) => event.kind === "message.created" && !deleted && !externalClaim).length;
|
|
3543
|
+
const inputs = Math.max(own.queued, unsettled);
|
|
3544
|
+
const outbound = own.sending + operations.length;
|
|
3545
|
+
const idle = own.turns === 0 && inputs === 0 && own.errands === 0 && outbound === 0;
|
|
3546
|
+
if (!idle) touch();
|
|
3547
|
+
return { idle, lastActiveAt, turns: own.turns, inputs, errands: own.errands, outbound };
|
|
3548
|
+
}
|
|
3549
|
+
__name(activity, "activity");
|
|
3550
|
+
function watchIdle() {
|
|
3551
|
+
if (idleTimeoutMs === void 0) return;
|
|
3552
|
+
let checking = false;
|
|
3553
|
+
const check = /* @__PURE__ */ __name(async () => {
|
|
3554
|
+
if (checking || stopping || idleCalled) return;
|
|
3555
|
+
const own = ownActivity();
|
|
3556
|
+
if (own.turns || own.queued || own.errands || own.sending) return touch();
|
|
3557
|
+
checking = true;
|
|
3558
|
+
try {
|
|
3559
|
+
const now = await activity();
|
|
3560
|
+
if (now.idle && !stopping && !idleCalled && Date.now() - lastActiveAt >= idleTimeoutMs) {
|
|
3561
|
+
idleCalled = true;
|
|
3562
|
+
try {
|
|
3563
|
+
onIdle(now);
|
|
3564
|
+
} catch (error) {
|
|
3565
|
+
warn("onIdle")(error);
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
} catch (error) {
|
|
3569
|
+
if (!stopping) warn("idle check")(error);
|
|
3570
|
+
} finally {
|
|
3571
|
+
checking = false;
|
|
3572
|
+
}
|
|
3573
|
+
}, "check");
|
|
3574
|
+
const timer = setInterval(() => void check(), Math.min(3e4, Math.max(1e3, Math.floor(idleTimeoutMs / 4))));
|
|
3575
|
+
timer.unref?.();
|
|
3576
|
+
detach.push(() => clearInterval(timer));
|
|
3577
|
+
}
|
|
3578
|
+
__name(watchIdle, "watchIdle");
|
|
3494
3579
|
const room = /* @__PURE__ */ __name((id) => {
|
|
3495
3580
|
let found = rooms.get(id);
|
|
3496
3581
|
if (!found) rooms.set(id, found = { id, queue: [], errands: [], controls: Promise.resolve(), lane: Promise.resolve() });
|
|
@@ -3569,9 +3654,21 @@ ${error instanceof Error ? error.message : String(error)}`;
|
|
|
3569
3654
|
}
|
|
3570
3655
|
__name(progress, "progress");
|
|
3571
3656
|
async function offer(event) {
|
|
3657
|
+
offering += 1;
|
|
3658
|
+
touch();
|
|
3659
|
+
try {
|
|
3660
|
+
await take(event);
|
|
3661
|
+
} finally {
|
|
3662
|
+
offering -= 1;
|
|
3663
|
+
touch();
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
__name(offer, "offer");
|
|
3667
|
+
async function take(event) {
|
|
3572
3668
|
const handle = await endpoint.acquireInbound(event.id);
|
|
3573
3669
|
if (!handle) return;
|
|
3574
3670
|
const roomId = event.data.conversationId ?? "";
|
|
3671
|
+
const clears = rooms.get(roomId)?.clears;
|
|
3575
3672
|
const input = await admitInput(event, handle).catch(async (error) => {
|
|
3576
3673
|
deferredNote(roomId)(error);
|
|
3577
3674
|
await handle.release("not-admitted-yet");
|
|
@@ -3580,11 +3677,12 @@ ${error instanceof Error ? error.message : String(error)}`;
|
|
|
3580
3677
|
if (!input) return;
|
|
3581
3678
|
if (stopping) return void await handle.release("runner-stopping");
|
|
3582
3679
|
const target = room(input.payload.conversationId);
|
|
3680
|
+
if (target.clears !== clears) return void await dispose([input], "cancel", "stop-and-clear");
|
|
3583
3681
|
target.queue.push(input);
|
|
3584
3682
|
requeue(target);
|
|
3585
3683
|
pump();
|
|
3586
3684
|
}
|
|
3587
|
-
__name(
|
|
3685
|
+
__name(take, "take");
|
|
3588
3686
|
async function admitInput(event, handle) {
|
|
3589
3687
|
const payload = event.data;
|
|
3590
3688
|
const { conversationId } = payload;
|
|
@@ -3735,11 +3833,13 @@ ${decision.reasonCode}`;
|
|
|
3735
3833
|
turn.live.end("ended");
|
|
3736
3834
|
if (candidate.turn === turn) candidate.turn = void 0;
|
|
3737
3835
|
running.delete(done);
|
|
3836
|
+
touch();
|
|
3738
3837
|
rooms.delete(candidate.id);
|
|
3739
3838
|
rooms.set(candidate.id, candidate);
|
|
3740
3839
|
pump();
|
|
3741
3840
|
});
|
|
3742
3841
|
running.add(done);
|
|
3842
|
+
touch();
|
|
3743
3843
|
}
|
|
3744
3844
|
while (localErrands.length && !stopping && running.size < maxTurns) startErrand(void 0, localErrands.shift());
|
|
3745
3845
|
}
|
|
@@ -4170,9 +4270,9 @@ ${JSON.stringify(result, null, 2)}`);
|
|
|
4170
4270
|
async function outcomeOf(turn, work) {
|
|
4171
4271
|
const signal = turn.controller.signal;
|
|
4172
4272
|
const settled = work.then((result) => ({ result }), (error) => ({ error }));
|
|
4173
|
-
const aborted = new Promise((
|
|
4174
|
-
if (signal.aborted)
|
|
4175
|
-
else signal.addEventListener("abort", () =>
|
|
4273
|
+
const aborted = new Promise((resolve4) => {
|
|
4274
|
+
if (signal.aborted) resolve4(void 0);
|
|
4275
|
+
else signal.addEventListener("abort", () => resolve4(void 0), { once: true });
|
|
4176
4276
|
});
|
|
4177
4277
|
const first = await Promise.race([settled, aborted]);
|
|
4178
4278
|
if (first && !signal.aborted) return first;
|
|
@@ -4414,8 +4514,8 @@ ${JSON.stringify(result, null, 2)}`);
|
|
|
4414
4514
|
...turn.facts ? { roomFacts: turn.facts } : {}
|
|
4415
4515
|
});
|
|
4416
4516
|
const line = /* @__PURE__ */ __name((message2, mark = "") => promptLine(message2, message2.senderId === agentId ? "You" : message2.senderName ?? message2.senderId, turn.media.get(message2.id), mark), "line");
|
|
4417
|
-
const
|
|
4418
|
-
const live = /* @__PURE__ */ __name(() =>
|
|
4517
|
+
const open5 = /* @__PURE__ */ __name(() => turn.open && !turn.controller.signal.aborted, "open");
|
|
4518
|
+
const live = /* @__PURE__ */ __name(() => open5() && !turn.silenced, "live");
|
|
4419
4519
|
return {
|
|
4420
4520
|
session: turn.session,
|
|
4421
4521
|
// §12.6: an errand turn's prompt is its errand; the owner's message is only its input's record.
|
|
@@ -4437,18 +4537,18 @@ ${JSON.stringify(result, null, 2)}`);
|
|
|
4437
4537
|
if (live()) progress(turn);
|
|
4438
4538
|
}, "onActivity"),
|
|
4439
4539
|
ask: /* @__PURE__ */ __name((request) => live() ? ask(roomId, turn, input.sender, request) : Promise.reject(turnEnded2()), "ask"),
|
|
4440
|
-
state: driverState(turn.session,
|
|
4540
|
+
state: driverState(turn.session, open5),
|
|
4441
4541
|
tools: tools2,
|
|
4442
|
-
confirmPickup: /* @__PURE__ */ __name(() => confirmPickup(roomId, turn,
|
|
4542
|
+
confirmPickup: /* @__PURE__ */ __name(() => confirmPickup(roomId, turn, open5), "confirmPickup")
|
|
4443
4543
|
};
|
|
4444
4544
|
}
|
|
4445
4545
|
__name(buildTurn, "buildTurn");
|
|
4446
|
-
async function confirmPickup(roomId, turn,
|
|
4447
|
-
if (!
|
|
4546
|
+
async function confirmPickup(roomId, turn, open5) {
|
|
4547
|
+
if (!open5()) throw turnEnded2();
|
|
4448
4548
|
turn.pickingUp = true;
|
|
4449
4549
|
try {
|
|
4450
4550
|
const current = await client.getConversation(roomId);
|
|
4451
|
-
if (!
|
|
4551
|
+
if (!open5()) throw turnEnded2();
|
|
4452
4552
|
if (!current?.memberIds?.includes(agentId)) {
|
|
4453
4553
|
loseMembership(room(roomId));
|
|
4454
4554
|
throw new Error("The agent is no longer a member of this conversation");
|
|
@@ -4460,7 +4560,7 @@ ${JSON.stringify(result, null, 2)}`);
|
|
|
4460
4560
|
}
|
|
4461
4561
|
throw new Error("Who can read this encrypted conversation changed");
|
|
4462
4562
|
}
|
|
4463
|
-
if (!
|
|
4563
|
+
if (!open5()) throw turnEnded2();
|
|
4464
4564
|
if (turn.pickUp) await turn.pickUp();
|
|
4465
4565
|
progress(turn);
|
|
4466
4566
|
} finally {
|
|
@@ -4552,9 +4652,13 @@ ${JSON.stringify(result, null, 2)}`);
|
|
|
4552
4652
|
async function control(current) {
|
|
4553
4653
|
const consumed = await client.consumeRuntimeSignal(current.id);
|
|
4554
4654
|
if (consumed.status !== "signal") return;
|
|
4655
|
+
touch();
|
|
4555
4656
|
const signal = consumed.signal;
|
|
4556
4657
|
const turn = current.turn;
|
|
4557
|
-
if (signal === "stop_and_drop" || signal === "new_session")
|
|
4658
|
+
if (signal === "stop_and_drop" || signal === "new_session") {
|
|
4659
|
+
current.clears = (current.clears ?? 0) + 1;
|
|
4660
|
+
void dispose(current.queue.splice(0), "cancel", "stop-and-clear");
|
|
4661
|
+
}
|
|
4558
4662
|
if (turn) {
|
|
4559
4663
|
void dispose(turn.inputs, "cancel", "stopped-before-submit");
|
|
4560
4664
|
abort(turn, signal === "interrupt" ? "stop" : signal === "stop_and_drop" ? "drop" : "new_session");
|
|
@@ -4568,6 +4672,7 @@ ${JSON.stringify(result, null, 2)}`);
|
|
|
4568
4672
|
const reset = Promise.all([...sessions].map((session) => resetSession(session).catch(warn("session reset")))).then(() => void 0);
|
|
4569
4673
|
await (current.resetting = reset.finally(() => {
|
|
4570
4674
|
current.resetting = void 0;
|
|
4675
|
+
touch();
|
|
4571
4676
|
pump();
|
|
4572
4677
|
}));
|
|
4573
4678
|
}
|
|
@@ -4611,6 +4716,7 @@ ${JSON.stringify(result, null, 2)}`);
|
|
|
4611
4716
|
})().catch(warn("restart recovery"));
|
|
4612
4717
|
target.recovering = work.finally(() => {
|
|
4613
4718
|
target.recovering = void 0;
|
|
4719
|
+
touch();
|
|
4614
4720
|
pump();
|
|
4615
4721
|
});
|
|
4616
4722
|
}
|
|
@@ -4738,7 +4844,11 @@ ${JSON.stringify(result, null, 2)}`);
|
|
|
4738
4844
|
if (sent !== "pending") finishErrandOutput(record, sent);
|
|
4739
4845
|
}, warn(`errand ${id}`));
|
|
4740
4846
|
posting.add(delivery);
|
|
4741
|
-
|
|
4847
|
+
touch();
|
|
4848
|
+
void delivery.finally(() => {
|
|
4849
|
+
posting.delete(delivery);
|
|
4850
|
+
touch();
|
|
4851
|
+
});
|
|
4742
4852
|
return true;
|
|
4743
4853
|
}
|
|
4744
4854
|
__name(takeErrand, "takeErrand");
|
|
@@ -4784,6 +4894,7 @@ ${JSON.stringify(result, null, 2)}`);
|
|
|
4784
4894
|
__name(saveErrand, "saveErrand");
|
|
4785
4895
|
function enqueueErrand(record) {
|
|
4786
4896
|
(record.origin ? room(record.origin).errands : localErrands).push(record);
|
|
4897
|
+
touch();
|
|
4787
4898
|
pump();
|
|
4788
4899
|
}
|
|
4789
4900
|
__name(enqueueErrand, "enqueueErrand");
|
|
@@ -4821,10 +4932,10 @@ ${JSON.stringify(result, null, 2)}`);
|
|
|
4821
4932
|
}
|
|
4822
4933
|
__name(contextPrompt, "contextPrompt");
|
|
4823
4934
|
function openErrands(roomId) {
|
|
4824
|
-
const
|
|
4825
|
-
const stale =
|
|
4935
|
+
const open5 = [...errandsById.values()].filter((record) => record.conversationId === roomId && openErrand(record));
|
|
4936
|
+
const stale = open5.filter((record) => Date.now() - Date.parse(record.repliedAt ?? record.reachedAt ?? record.updatedAt) > ERRAND_OPEN_MS);
|
|
4826
4937
|
for (const record of stale) saveErrand({ ...record, status: "expired", reason: "no reply within a week" });
|
|
4827
|
-
return
|
|
4938
|
+
return open5.filter((record) => !stale.includes(record));
|
|
4828
4939
|
}
|
|
4829
4940
|
__name(openErrands, "openErrands");
|
|
4830
4941
|
function replied(turn) {
|
|
@@ -4859,6 +4970,7 @@ ${JSON.stringify(result, null, 2)}`);
|
|
|
4859
4970
|
if (holder.turn === turn) holder.turn = void 0;
|
|
4860
4971
|
errandTurns.delete(record.id);
|
|
4861
4972
|
running.delete(done);
|
|
4973
|
+
touch();
|
|
4862
4974
|
if (origin) {
|
|
4863
4975
|
rooms.delete(origin.id);
|
|
4864
4976
|
rooms.set(origin.id, origin);
|
|
@@ -4866,6 +4978,7 @@ ${JSON.stringify(result, null, 2)}`);
|
|
|
4866
4978
|
pump();
|
|
4867
4979
|
});
|
|
4868
4980
|
running.add(done);
|
|
4981
|
+
touch();
|
|
4869
4982
|
}
|
|
4870
4983
|
__name(startErrand, "startErrand");
|
|
4871
4984
|
async function executeErrand(current, turn, record) {
|
|
@@ -5247,7 +5360,10 @@ ${JSON.stringify(result, null, 2)}`);
|
|
|
5247
5360
|
}
|
|
5248
5361
|
warn("recovery")(error);
|
|
5249
5362
|
}, "onError") }));
|
|
5363
|
+
touch();
|
|
5364
|
+
watchIdle();
|
|
5250
5365
|
},
|
|
5366
|
+
activity,
|
|
5251
5367
|
stop() {
|
|
5252
5368
|
stopping ??= (async () => {
|
|
5253
5369
|
for (const off of detach.splice(0).reverse()) off();
|
|
@@ -5275,13 +5391,14 @@ ${JSON.stringify(result, null, 2)}`);
|
|
|
5275
5391
|
},
|
|
5276
5392
|
async communicate(input) {
|
|
5277
5393
|
if (!client) throw new Error("Start the runner before sending");
|
|
5278
|
-
return client.communicate(parseCommunicateToolInput(input));
|
|
5394
|
+
return activeCall(() => client.communicate(parseCommunicateToolInput(input)));
|
|
5279
5395
|
},
|
|
5280
5396
|
async relayRead(conversationId, page = {}) {
|
|
5397
|
+
touch();
|
|
5281
5398
|
const limit = page.limit ?? 20;
|
|
5282
5399
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) throw new ConnectorRelayError(400, "CONNECTOR_RELAY_INVALID", "limit must be 1 to 100");
|
|
5283
5400
|
const deadline = page.deadline ?? Date.now() + CONNECTOR_RELAY_ANSWER_WITHIN_MS;
|
|
5284
|
-
return beforeDeadline((async () => {
|
|
5401
|
+
return beforeDeadline(activeCall(async () => {
|
|
5285
5402
|
const { ownerId } = await relayRoom(conversationId);
|
|
5286
5403
|
const result = await readAgain(() => endpoint.query("list_messages", { conversationId, limit, ...page.before ? { before: page.before } : {} }), deadline);
|
|
5287
5404
|
const accepted = result.messages.filter((message2) => !message2.deleted && typeof message2.text === "string" && message2.status === "sent" && Number.isSafeInteger(message2.mlsFeedOrdinal) && message2.mlsFeedOrdinal > 0);
|
|
@@ -5293,9 +5410,10 @@ ${JSON.stringify(result, null, 2)}`);
|
|
|
5293
5410
|
nextBefore = String(accepted.at(-1).mlsFeedOrdinal);
|
|
5294
5411
|
}
|
|
5295
5412
|
return { messages, nextBefore };
|
|
5296
|
-
})
|
|
5413
|
+
}), deadline);
|
|
5297
5414
|
},
|
|
5298
5415
|
async relaySend(conversationId, messageId, text3, options2 = {}) {
|
|
5416
|
+
touch();
|
|
5299
5417
|
if (typeof messageId !== "string" || !ENDPOINT_ID_PATTERN.test(messageId) || messageId.length > 120 || typeof text3 !== "string" || !text3.trim() || Buffer.byteLength(text3) > MLS_DEV_TEXT_MAX_BYTES) {
|
|
5300
5418
|
throw new ConnectorRelayError(400, "CONNECTOR_RELAY_INVALID", "A send needs a messageId and 1 byte to 32 KiB of text");
|
|
5301
5419
|
}
|
|
@@ -5303,13 +5421,14 @@ ${JSON.stringify(result, null, 2)}`);
|
|
|
5303
5421
|
const id = `relay-send-${sha2563(JSON.stringify([conversationId, messageId])).slice(0, 32)}`;
|
|
5304
5422
|
let entry = relaySends.get(id);
|
|
5305
5423
|
if (!entry) relaySends.set(id, entry = prepareRelaySend(conversationId, id, text3, deadline));
|
|
5306
|
-
const prepared = await beforeDeadline(entry.ready, deadline);
|
|
5424
|
+
const prepared = await beforeDeadline(activeCall(() => entry.ready), deadline);
|
|
5307
5425
|
if (prepared.text !== text3) throw new ConnectorRelayError(409, "CONNECTOR_RELAY_MESSAGE_CONFLICT", "This messageId was already sent with other text");
|
|
5308
|
-
const sent = await within(entry.sent, Math.max(0, deadline - Date.now()));
|
|
5426
|
+
const sent = await within(activeCall(() => entry.sent), Math.max(0, deadline - Date.now()));
|
|
5309
5427
|
if (sent === "refused") throw new ConnectorRelayError(409, "CONNECTOR_RELAY_SEND_REFUSED", "Canon did not accept the message; the conversation's members may have changed");
|
|
5310
5428
|
return { conversationId, messageId, historyMessageId: id, status: sent === "delivered" ? "sent" : "pending" };
|
|
5311
5429
|
},
|
|
5312
5430
|
async relayStart(targetUserId, messageId, text3, selection, startOptions = {}) {
|
|
5431
|
+
touch();
|
|
5313
5432
|
if (typeof targetUserId !== "string" || !targetUserId || targetUserId.length > 200 || targetUserId.includes("/") || typeof messageId !== "string" || !ENDPOINT_ID_PATTERN.test(messageId) || messageId.length > 120 || typeof text3 !== "string" || !text3.trim() || Buffer.byteLength(text3) > MLS_DEV_TEXT_MAX_BYTES) {
|
|
5314
5433
|
throw new ConnectorRelayError(400, "CONNECTOR_RELAY_INVALID", "A start needs a person or agent, a messageId and 1 byte to 32 KiB of text");
|
|
5315
5434
|
}
|
|
@@ -5318,12 +5437,12 @@ ${JSON.stringify(result, null, 2)}`);
|
|
|
5318
5437
|
const deadline = startOptions.deadline ?? Date.now() + CONNECTOR_RELAY_ANSWER_WITHIN_MS;
|
|
5319
5438
|
const identity = { messageId, historyMessageId: `relay-start-${sha2563(messageId).slice(0, 32)}` };
|
|
5320
5439
|
const answer = await within(
|
|
5321
|
-
endpoint.startWithheldOpener({
|
|
5440
|
+
activeCall(() => endpoint.startWithheldOpener({
|
|
5322
5441
|
targetUserId,
|
|
5323
5442
|
text: text3,
|
|
5324
5443
|
messageId: identity.historyMessageId,
|
|
5325
5444
|
...selection ? { selection } : {}
|
|
5326
|
-
}).then((result2) => ({ result: result2 }), (error) => ({ error })),
|
|
5445
|
+
})).then((result2) => ({ result: result2 }), (error) => ({ error })),
|
|
5327
5446
|
Math.max(0, deadline - Date.now())
|
|
5328
5447
|
);
|
|
5329
5448
|
if (!answer || "error" in answer && answer.error?.retryable === true) {
|
|
@@ -5373,6 +5492,81 @@ function relayMessage(message2, ownerId) {
|
|
|
5373
5492
|
}
|
|
5374
5493
|
__name(relayMessage, "relayMessage");
|
|
5375
5494
|
|
|
5495
|
+
// src/runner/wake.ts
|
|
5496
|
+
import { constants as constants5 } from "node:fs";
|
|
5497
|
+
import { open as open4, realpath as realpath5 } from "node:fs/promises";
|
|
5498
|
+
import { isAbsolute as isAbsolute4, resolve as resolve3 } from "node:path";
|
|
5499
|
+
var MAX_WAKE_CONFIGURATION_BYTES = 16 * 1024;
|
|
5500
|
+
var MAX_WAKE_ROOMS = 32;
|
|
5501
|
+
var ROOM_ID = /^[A-Za-z0-9_-]{1,200}$/;
|
|
5502
|
+
function isWakeSecret(value) {
|
|
5503
|
+
if (typeof value !== "string" || !/^whsec_[A-Za-z0-9+/]+={0,2}$/.test(value)) return false;
|
|
5504
|
+
const key = Buffer.from(value.slice(6), "base64");
|
|
5505
|
+
return key.length >= 32 && key.length <= 64 && key.toString("base64") === value.slice(6);
|
|
5506
|
+
}
|
|
5507
|
+
__name(isWakeSecret, "isWakeSecret");
|
|
5508
|
+
function parseWakeConfiguration(value) {
|
|
5509
|
+
const invalid2 = /* @__PURE__ */ __name((what) => {
|
|
5510
|
+
throw new Error(`Invalid wake configuration: ${what}`);
|
|
5511
|
+
}, "invalid");
|
|
5512
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return invalid2("expected a JSON object");
|
|
5513
|
+
const v = value;
|
|
5514
|
+
const keys = ["version", "callbackUrl", "secret", "conversationIds"];
|
|
5515
|
+
if (Object.keys(v).sort().join() !== [...keys].sort().join()) invalid2(`expected ${keys.join(", ")}`);
|
|
5516
|
+
if (v.version !== 1) invalid2("version");
|
|
5517
|
+
let url;
|
|
5518
|
+
try {
|
|
5519
|
+
url = typeof v.callbackUrl === "string" && v.callbackUrl.length <= 2048 ? new URL(v.callbackUrl) : void 0;
|
|
5520
|
+
} catch {
|
|
5521
|
+
}
|
|
5522
|
+
if (!url || url.protocol !== "https:" || url.username || url.password || url.hash) invalid2("callbackUrl: a public https URL, with no credentials or fragment");
|
|
5523
|
+
if (!isWakeSecret(v.secret)) invalid2("secret: whsec_ and the padded base64 of 32 to 64 random bytes");
|
|
5524
|
+
const rooms = v.conversationIds;
|
|
5525
|
+
if (!Array.isArray(rooms) || rooms.length < 1 || rooms.length > MAX_WAKE_ROOMS || rooms.some((id) => typeof id !== "string" || !ROOM_ID.test(id))) {
|
|
5526
|
+
invalid2(`conversationIds: 1 to ${MAX_WAKE_ROOMS} conversation ids`);
|
|
5527
|
+
}
|
|
5528
|
+
return { callbackUrl: url.toString(), secret: v.secret, conversationIds: [...new Set(rooms)].sort() };
|
|
5529
|
+
}
|
|
5530
|
+
__name(parseWakeConfiguration, "parseWakeConfiguration");
|
|
5531
|
+
async function loadWakeConfiguration(path) {
|
|
5532
|
+
const fail2 = /* @__PURE__ */ __name(() => {
|
|
5533
|
+
throw new Error(`The wake configuration must be a private file (0600, owned by this user, not a link): ${path}`);
|
|
5534
|
+
}, "fail");
|
|
5535
|
+
if (typeof path !== "string" || !isAbsolute4(path) || path.includes("\0") || resolve3(path) !== path || await realpath5(path).catch(() => "") !== path) fail2();
|
|
5536
|
+
const windows = process.platform === "win32";
|
|
5537
|
+
if (windows) assertPrivateWindowsFile(path);
|
|
5538
|
+
const file = await open4(path, constants5.O_RDONLY | (constants5.O_NOFOLLOW ?? 0) | (constants5.O_NONBLOCK ?? 0)).catch(() => fail2());
|
|
5539
|
+
let text3;
|
|
5540
|
+
try {
|
|
5541
|
+
const info = await file.stat();
|
|
5542
|
+
if (!info.isFile() || info.nlink !== 1 || info.size < 1 || info.size > MAX_WAKE_CONFIGURATION_BYTES) fail2();
|
|
5543
|
+
if (!windows && (info.uid !== process.getuid?.() || (info.mode & 63) !== 0)) fail2();
|
|
5544
|
+
const bytes = await readBoundedRegularFileHandle(file, MAX_WAKE_CONFIGURATION_BYTES).catch(() => fail2());
|
|
5545
|
+
try {
|
|
5546
|
+
text3 = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
5547
|
+
} catch {
|
|
5548
|
+
return fail2();
|
|
5549
|
+
}
|
|
5550
|
+
} finally {
|
|
5551
|
+
await file.close();
|
|
5552
|
+
}
|
|
5553
|
+
let value;
|
|
5554
|
+
try {
|
|
5555
|
+
value = JSON.parse(text3);
|
|
5556
|
+
} catch {
|
|
5557
|
+
throw new Error("Invalid wake configuration: not JSON");
|
|
5558
|
+
}
|
|
5559
|
+
return parseWakeConfiguration(value);
|
|
5560
|
+
}
|
|
5561
|
+
__name(loadWakeConfiguration, "loadWakeConfiguration");
|
|
5562
|
+
function parseIdleTimeout(value) {
|
|
5563
|
+
const match = /^(\d{1,7})([smh]?)$/.exec(value.trim());
|
|
5564
|
+
const ms = match ? Number(match[1]) * { "": 1e3, s: 1e3, m: 6e4, h: 36e5 }[match[2]] : NaN;
|
|
5565
|
+
if (!Number.isSafeInteger(ms) || ms < 1e3) throw new Error("--idle-timeout takes a duration of at least 1 second: 90, 90s, 10m or 1h.");
|
|
5566
|
+
return ms;
|
|
5567
|
+
}
|
|
5568
|
+
__name(parseIdleTimeout, "parseIdleTimeout");
|
|
5569
|
+
|
|
5376
5570
|
export {
|
|
5377
5571
|
DriverSessionError,
|
|
5378
5572
|
ConnectorRelayError,
|
|
@@ -5404,5 +5598,8 @@ export {
|
|
|
5404
5598
|
writeRelayCredential,
|
|
5405
5599
|
relayRunnerHome,
|
|
5406
5600
|
runtimeDescriptor,
|
|
5407
|
-
createRunner
|
|
5601
|
+
createRunner,
|
|
5602
|
+
parseWakeConfiguration,
|
|
5603
|
+
loadWakeConfiguration,
|
|
5604
|
+
parseIdleTimeout
|
|
5408
5605
|
};
|
package/dist/cli.js
CHANGED
|
@@ -17,13 +17,15 @@ import {
|
|
|
17
17
|
loadInstructions,
|
|
18
18
|
loadMlsDevConfiguration,
|
|
19
19
|
loadRelayConfiguration,
|
|
20
|
+
loadWakeConfiguration,
|
|
21
|
+
parseIdleTimeout,
|
|
20
22
|
queueErrand,
|
|
21
23
|
readRelayCredential,
|
|
22
24
|
relayRunnerHome,
|
|
23
25
|
runMlsDevSetup,
|
|
24
26
|
startRelayChannel,
|
|
25
27
|
writeRelayCredential
|
|
26
|
-
} from "./chunks/chunk-
|
|
28
|
+
} from "./chunks/chunk-IEKVS7AN.js";
|
|
27
29
|
import {
|
|
28
30
|
assertPrivateHome,
|
|
29
31
|
runnerLockName
|
|
@@ -56,10 +58,10 @@ var RUNNER_VERSION = readCliPackageVersion(import.meta.url);
|
|
|
56
58
|
var RUNNER_CLIENT = isCanonClientVersion(RUNNER_VERSION) ? { kind: "runner", version: RUNNER_VERSION } : void 0;
|
|
57
59
|
function once(handler) {
|
|
58
60
|
let called = false;
|
|
59
|
-
return (
|
|
61
|
+
return (...args) => {
|
|
60
62
|
if (!called) {
|
|
61
63
|
called = true;
|
|
62
|
-
handler(
|
|
64
|
+
handler(...args);
|
|
63
65
|
}
|
|
64
66
|
};
|
|
65
67
|
}
|
|
@@ -97,6 +99,14 @@ command also takes --mls-dev-config <path>, an operator's own custody instead. W
|
|
|
97
99
|
of these, the agent refuses every room, and where Canon encrypts every new conversation
|
|
98
100
|
it does not start.
|
|
99
101
|
|
|
102
|
+
Waking and sleeping: code, claude, codex and acp also take [--wake-config <path> |
|
|
103
|
+
--wake-off] [--idle-timeout <duration>]. With --wake-config the agent registers, at
|
|
104
|
+
each start, a public HTTPS callback for up to 32 rooms; Canon POSTs it a signed,
|
|
105
|
+
content-free hint when a person types or a message arrives there, so the host can
|
|
106
|
+
start (or resume) the agent. With --idle-timeout the agent stops and exits 0 once it
|
|
107
|
+
has had nothing to do for that long: no turn, no input, no errand, nothing Canon has
|
|
108
|
+
yet to accept. Nothing is lost while it is stopped; it answers once it starts again.
|
|
109
|
+
|
|
100
110
|
Team rooms (an owner's agents working together): claude, codex and acp also take
|
|
101
111
|
[--instructions <file>] [--trust-agent <agentId>]...; every run command takes
|
|
102
112
|
[--agent-turn-cap <n>] [--mention-alias <name>]....
|
|
@@ -174,6 +184,17 @@ Flags:
|
|
|
174
184
|
$CANON_HOME/relay. The relay serves only encrypted rooms,
|
|
175
185
|
which are then not end-to-end encrypted to that agent. It sets
|
|
176
186
|
up the relay's device with that credential.
|
|
187
|
+
--wake-config <path> Register the agent's wake callback at start: a private JSON file
|
|
188
|
+
(0600, owned by you, not a link) {"version": 1, "callbackUrl":
|
|
189
|
+
"https://...", "secret": "whsec_<base64 of 32 random bytes>",
|
|
190
|
+
"conversationIds": [up to 32 room ids]}. Canon checks the URL
|
|
191
|
+
with a signed challenge first; the agent does not start if
|
|
192
|
+
that fails. Without the flag a registration is left as it is.
|
|
193
|
+
--wake-off Remove the agent's wake callback at start.
|
|
194
|
+
--idle-timeout <d> Stop and exit 0 after this long with nothing to do: whole
|
|
195
|
+
seconds, or 90s, 10m, 1h. Typing in a room does not reach the
|
|
196
|
+
agent: a host that also receives typing hints decides itself
|
|
197
|
+
whether to keep the agent awake for them.
|
|
177
198
|
--mls-dev-config <p> An operator's own MLS configuration, instead of the device the
|
|
178
199
|
runner keeps itself: an absolute path to a private JSON file
|
|
179
200
|
with the environment's issuer pins and the custody helper (no
|
|
@@ -263,6 +284,16 @@ async function main() {
|
|
|
263
284
|
if (values["guest-mode"] !== void 0 && values["unguarded-guests"]) throw new UsageError("--guest-mode and --unguarded-guests are alternatives.");
|
|
264
285
|
const team = teamOptions(values);
|
|
265
286
|
if (values.driver === "connector" && values["allow-session-for-destructive"]) throw new UsageError("--allow-session-for-destructive goes with a driver that asks.");
|
|
287
|
+
if (values.driver === "connector" && (values["wake-config"] !== void 0 || values["wake-off"] || values["idle-timeout"] !== void 0)) {
|
|
288
|
+
throw new UsageError("--wake-config, --wake-off and --idle-timeout go with an agent's own key, not --driver connector.");
|
|
289
|
+
}
|
|
290
|
+
if (values["wake-config"] !== void 0 && values["wake-off"]) throw new UsageError("--wake-config and --wake-off are alternatives.");
|
|
291
|
+
let idleTimeoutMs;
|
|
292
|
+
try {
|
|
293
|
+
idleTimeoutMs = values["idle-timeout"] !== void 0 ? parseIdleTimeout(values["idle-timeout"]) : void 0;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
throw new UsageError(error.message);
|
|
296
|
+
}
|
|
266
297
|
if (values.driver === "connector") return runRelay(values, team);
|
|
267
298
|
const permissionMode = values["permission-mode"];
|
|
268
299
|
if (permissionMode !== void 0 && !PERMISSION_MODES.includes(permissionMode)) {
|
|
@@ -278,6 +309,7 @@ async function main() {
|
|
|
278
309
|
const trustedAgents = values["trust-agent"] ?? [];
|
|
279
310
|
if (trustedAgents.some((id) => !/^[\w.:-]{1,200}$/.test(id))) throw new UsageError("--trust-agent takes an agent id.");
|
|
280
311
|
const instructions = values.instructions !== void 0 ? await loadInstructions(resolve(values.instructions)) : void 0;
|
|
312
|
+
const wake = values["wake-off"] ? null : values["wake-config"] !== void 0 ? await loadWakeConfiguration(resolve(values["wake-config"])) : void 0;
|
|
281
313
|
const { environmentId, home, profile, apiKey } = await settings(values);
|
|
282
314
|
const mls = await mlsDevice(values, environmentId, home);
|
|
283
315
|
const baseUrl = process.env.CANON_BASE_URL || profile?.baseUrl;
|
|
@@ -335,6 +367,15 @@ async function main() {
|
|
|
335
367
|
...trustedAgents.length ? { trustedAgents } : {},
|
|
336
368
|
...values["allow-session-for-destructive"] ? { sessionApprovalForDestructive: true } : {},
|
|
337
369
|
...RUNNER_CLIENT ? { client: RUNNER_CLIENT } : {},
|
|
370
|
+
...wake !== void 0 ? { wake } : {},
|
|
371
|
+
// Idle: a clean stop and exit 0, so a supervisor that restarts only on failure leaves the agent stopped until it is woken.
|
|
372
|
+
...idleTimeoutMs !== void 0 ? { idleTimeoutMs, onIdle: once(() => {
|
|
373
|
+
console.error(`canon-agent: nothing to do for ${values["idle-timeout"]}; stopping.`);
|
|
374
|
+
runner.stop().then(() => process.exit(0), (error) => {
|
|
375
|
+
console.error(`canon-agent: ${error instanceof Error ? error.message : String(error)}`);
|
|
376
|
+
process.exit(1);
|
|
377
|
+
});
|
|
378
|
+
}) } : {},
|
|
338
379
|
onFatal: once((error) => {
|
|
339
380
|
console.error(`canon-agent: ${error.message}`);
|
|
340
381
|
void runner.stop().catch(() => void 0).finally(() => process.exit(1));
|
|
@@ -347,7 +388,7 @@ async function main() {
|
|
|
347
388
|
await runner.stop().catch(() => void 0);
|
|
348
389
|
throw error;
|
|
349
390
|
}
|
|
350
|
-
console.error(`canon-agent: running the ${values.driver} driver in ${environmentId}. Ctrl-C stops it.`);
|
|
391
|
+
console.error(`canon-agent: running the ${values.driver} driver in ${environmentId}. Ctrl-C stops it${idleTimeoutMs !== void 0 ? `, and it stops itself after ${values["idle-timeout"]} with nothing to do` : ""}.`);
|
|
351
392
|
}
|
|
352
393
|
__name(main, "main");
|
|
353
394
|
async function runRelay(values, team) {
|
|
@@ -478,7 +519,10 @@ function parse() {
|
|
|
478
519
|
why: { type: "string" },
|
|
479
520
|
origin: { type: "string" },
|
|
480
521
|
follow: { type: "boolean" },
|
|
481
|
-
cancel: { type: "string" }
|
|
522
|
+
cancel: { type: "string" },
|
|
523
|
+
"wake-config": { type: "string" },
|
|
524
|
+
"wake-off": { type: "boolean" },
|
|
525
|
+
"idle-timeout": { type: "string" }
|
|
482
526
|
}
|
|
483
527
|
});
|
|
484
528
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { createRunner, runtimeDescriptor } from './runner/runner.js';
|
|
2
|
-
export type { Runner, RunnerOptions } from './runner/runner.js';
|
|
2
|
+
export type { Runner, RunnerActivity, RunnerOptions } from './runner/runner.js';
|
|
3
|
+
export { loadWakeConfiguration, parseWakeConfiguration } from './runner/wake.js';
|
|
3
4
|
export { ConnectorRelayError, DriverSessionError } from './runner/types.js';
|
|
4
5
|
export { loadMlsDevConfiguration, MLS_SETUP_ACTIONS, mlsDevRoomPackageId, runMlsDevSetup } from './runner/mls.js';
|
|
5
6
|
export type { MlsSetupAction, RunnerMlsOptions } from './runner/mls.js';
|
package/dist/index.js
CHANGED
|
@@ -18,13 +18,15 @@ import {
|
|
|
18
18
|
loadClaudeQuery,
|
|
19
19
|
loadMlsDevConfiguration,
|
|
20
20
|
loadRelayConfiguration,
|
|
21
|
+
loadWakeConfiguration,
|
|
21
22
|
managedMlsDevice,
|
|
22
23
|
mlsDevRoomPackageId,
|
|
24
|
+
parseWakeConfiguration,
|
|
23
25
|
queueErrand,
|
|
24
26
|
runMlsDevSetup,
|
|
25
27
|
runtimeDescriptor,
|
|
26
28
|
startRelayChannel
|
|
27
|
-
} from "./chunks/chunk-
|
|
29
|
+
} from "./chunks/chunk-IEKVS7AN.js";
|
|
28
30
|
import "./chunks/chunk-DTAWEQCM.js";
|
|
29
31
|
import {
|
|
30
32
|
CanonApiError,
|
|
@@ -88,11 +90,13 @@ export {
|
|
|
88
90
|
loadClaudeQuery,
|
|
89
91
|
loadMlsDevConfiguration,
|
|
90
92
|
loadRelayConfiguration,
|
|
93
|
+
loadWakeConfiguration,
|
|
91
94
|
managedMlsDevice,
|
|
92
95
|
materializeAttachment,
|
|
93
96
|
materializeMessageMedia,
|
|
94
97
|
materializeReplyContextMedia,
|
|
95
98
|
mlsDevRoomPackageId,
|
|
99
|
+
parseWakeConfiguration,
|
|
96
100
|
queueErrand,
|
|
97
101
|
resolveAttachmentMimeType,
|
|
98
102
|
resumeRegistration,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { Endpoint } from './engine.js';
|
|
2
2
|
/** Transient writes are coalesced here; they never enter the durable outbox. */
|
|
3
3
|
export declare function createEndpointTransientPublisher(endpoint: Pick<Endpoint, 'publish'>, clock?: () => number): {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
/** A keystroke in the composer wakes the room's agents: once per burst of typing (a 3 s pause ends one), and every 10 s
|
|
5
|
+
* of it. The hint carries no content; typing itself never leaves the device. */
|
|
6
|
+
prewarmAgents(conversationId: string): void;
|
|
6
7
|
setPresence(state: "online" | "offline" | "hidden"): void;
|
|
7
8
|
close(): void;
|
|
8
9
|
};
|
package/dist/runner/runner.d.ts
CHANGED
|
@@ -68,9 +68,35 @@ export interface RunnerOptions {
|
|
|
68
68
|
sessionApprovalForDestructive?: boolean;
|
|
69
69
|
/** How long running turns may finish on shutdown before they are aborted (default 10 s). */
|
|
70
70
|
shutdownGraceMs?: number;
|
|
71
|
+
/**
|
|
72
|
+
* Idle (agent-wake.md): `onIdle` is called once the runner has been idle (`activity().idle`) for this long, in ms, and
|
|
73
|
+
* again only after new activity and another idle period. The runner does nothing else about it: the host decides, as the
|
|
74
|
+
* CLI's --idle-timeout stops and exits, or a hosting platform suspends the agent's machine until Canon's wake callback.
|
|
75
|
+
*/
|
|
76
|
+
idleTimeoutMs?: number;
|
|
77
|
+
/** Idle: called with the activity that showed it; it never names a room, message or content. */
|
|
78
|
+
onIdle?(activity: RunnerActivity): void;
|
|
71
79
|
/** Never receives prompt or reply text. */
|
|
72
80
|
log?: (line: string) => void;
|
|
73
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* What the runner has in hand, for a host that stops or suspends an idle agent (agent-wake.md). Counts only. Idle is all
|
|
84
|
+
* of them 0: once stopped or suspended, the agent loses nothing (input waits in Canon and in its inbox) but answers only
|
|
85
|
+
* when Canon's wake callback, or its host, starts it again.
|
|
86
|
+
*/
|
|
87
|
+
export interface RunnerActivity {
|
|
88
|
+
idle: boolean;
|
|
89
|
+
/** When the runner last had something to do (epoch ms), or when it started. */
|
|
90
|
+
lastActiveAt: number;
|
|
91
|
+
/** Turns running or starting, errand turns, restart recovery and New session resets included. */
|
|
92
|
+
turns: number;
|
|
93
|
+
/** Input waiting for a turn: in the inbox, being admitted, or queued behind a running turn (whose own input is in `turns`). */
|
|
94
|
+
inputs: number;
|
|
95
|
+
/** Errands waiting for their turn. */
|
|
96
|
+
errands: number;
|
|
97
|
+
/** In-flight proactive/relay calls plus operations Canon has not answered, including replies, cards and group changes. */
|
|
98
|
+
outbound: number;
|
|
99
|
+
}
|
|
74
100
|
export interface Runner {
|
|
75
101
|
/** Connects and starts taking input; resolves once listening is set up. */
|
|
76
102
|
start(): Promise<void>;
|
|
@@ -78,6 +104,8 @@ export interface Runner {
|
|
|
78
104
|
stop(): Promise<void>;
|
|
79
105
|
/** A proactive send outside any turn (cron): the `communicate` tool's handler, under the runner's identity. */
|
|
80
106
|
communicate(input: CommunicateInput): Promise<CommunicateResult>;
|
|
107
|
+
/** What the runner has in hand now, for a host's idle policy (agent-wake.md). Only between start and stop. */
|
|
108
|
+
activity(): Promise<RunnerActivity>;
|
|
81
109
|
/**
|
|
82
110
|
* Relay mode (§11.6): an encrypted room's history as this device verified it and Canon accepted it, newest first, cut
|
|
83
111
|
* to the channel's response budget. `deadline` (epoch ms, default the channel's answer budget) bounds the read.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AgentWakeConfiguration } from '../internal/backend-contracts/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* A wake callback's signing secret: `whsec_` and the standard base64 (padded) of 32 to 64 random bytes. Canon takes at
|
|
4
|
+
* least 32 for a wake callback, and a receiver such as Agent Platform's `/wake/:bindingId` at most 64, padded.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isWakeSecret(value: unknown): value is string;
|
|
7
|
+
/** The same checks Canon makes before it sends the callback its verification challenge; Canon checks again. */
|
|
8
|
+
export declare function parseWakeConfiguration(value: unknown): AgentWakeConfiguration;
|
|
9
|
+
/**
|
|
10
|
+
* `--wake-config <path>` (agent-wake.md): the agent's signed wake callback. A private JSON file (0600, owned by this user,
|
|
11
|
+
* one name, no links; on Windows only this user, SYSTEM and Administrators may reach it and its folder), since it holds
|
|
12
|
+
* the callback's signing secret: `{ "version": 1, "callbackUrl", "secret", "conversationIds" }`. Its text is never logged.
|
|
13
|
+
*/
|
|
14
|
+
export declare function loadWakeConfiguration(path: string): Promise<AgentWakeConfiguration>;
|
|
15
|
+
/** `--idle-timeout`: whole seconds, or a number with `s`, `m` or `h`; at least one second. Returns milliseconds. */
|
|
16
|
+
export declare function parseIdleTimeout(value: string): number;
|