@intentface/latch-core 0.11.0 → 0.13.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/dist/models/catalog.d.ts +7 -0
- package/dist/models/catalog.d.ts.map +1 -1
- package/dist/models/catalog.js +15 -5
- package/dist/models/catalog.js.map +1 -1
- package/dist/principal.d.ts +13 -1
- package/dist/principal.d.ts.map +1 -1
- package/dist/runtime.d.ts +73 -16
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +215 -35
- package/dist/runtime.js.map +1 -1
- package/dist/storage.d.ts +29 -1
- package/dist/storage.d.ts.map +1 -1
- package/package.json +8 -8
package/dist/runtime.js
CHANGED
|
@@ -504,6 +504,23 @@ function fireAndForget(fn) {
|
|
|
504
504
|
*/
|
|
505
505
|
/** Keys whose values are masked when a non-`Error` throw is serialized. */
|
|
506
506
|
const SECRETISH_KEY = /token|secret|password|authorization|cookie|api[-_]?key|credential/i;
|
|
507
|
+
/** The last `error` chunk's text in a UI message stream body (SSE `data:` lines), if any. */
|
|
508
|
+
function lastStreamError(body) {
|
|
509
|
+
let error;
|
|
510
|
+
for (const line of body.split("\n")) {
|
|
511
|
+
if (!line.startsWith("data: "))
|
|
512
|
+
continue;
|
|
513
|
+
try {
|
|
514
|
+
const chunk = JSON.parse(line.slice(6));
|
|
515
|
+
if (chunk.type === "error" && chunk.errorText)
|
|
516
|
+
error = chunk.errorText;
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
// "[DONE]" and other non-JSON lines
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return error;
|
|
523
|
+
}
|
|
507
524
|
export function clientErrorMessage(error) {
|
|
508
525
|
const message = error instanceof Error ? error.message.trim() : "";
|
|
509
526
|
if (message)
|
|
@@ -554,13 +571,15 @@ export function createRuntime(config) {
|
|
|
554
571
|
"Implement openTurn — see the StorageAdapter contract.");
|
|
555
572
|
}
|
|
556
573
|
/** Resolve an agent's per-request config + a display model id. */
|
|
557
|
-
async function resolveAgentConfig(agentName, principal, request, turnContext
|
|
574
|
+
async function resolveAgentConfig(agentName, principal, request, turnContext,
|
|
575
|
+
/** Passed through to `dynamicAgents.resolve` (see DynamicAgentResolveContext). */
|
|
576
|
+
resolveCtx) {
|
|
558
577
|
const runtimeCtx = await config.context.build({ principal, request, turnContext });
|
|
559
578
|
// Code-declared agents first; otherwise a dynamic (e.g. DB-stored) agent.
|
|
560
579
|
const factory = config.agents[agentName];
|
|
561
580
|
const cfg = factory
|
|
562
581
|
? await factory({ context: runtimeCtx, principal, turnContext })
|
|
563
|
-
: await config.dynamicAgents?.resolve(agentName, principal);
|
|
582
|
+
: await config.dynamicAgents?.resolve(agentName, principal, resolveCtx);
|
|
564
583
|
if (!cfg)
|
|
565
584
|
throw new Error(`Unknown agent: ${agentName}`);
|
|
566
585
|
const modelId = typeof cfg.model === "string"
|
|
@@ -576,7 +595,16 @@ export function createRuntime(config) {
|
|
|
576
595
|
* server-side (resume).
|
|
577
596
|
*/
|
|
578
597
|
async function buildTurn(args) {
|
|
579
|
-
const { resolvedChatId, principal,
|
|
598
|
+
const { resolvedChatId, principal, run, lease, runtimeCtx } = args;
|
|
599
|
+
// A per-turn model override replaces the agent's own, and the telemetry id
|
|
600
|
+
// with it — a run row that says the agent's model while a different one
|
|
601
|
+
// answered makes every later comparison a lie.
|
|
602
|
+
const cfg = args.model ? { ...args.cfg, model: args.model } : args.cfg;
|
|
603
|
+
const modelId = args.model
|
|
604
|
+
? typeof args.model === "string"
|
|
605
|
+
? args.model
|
|
606
|
+
: (args.model.modelId ?? "unknown")
|
|
607
|
+
: args.modelId;
|
|
580
608
|
// Mutable: the start moments below may inject, replace or rewrite history
|
|
581
609
|
// before the model runs. From here on this is the turn's transcript.
|
|
582
610
|
let uiMessages = args.uiMessages;
|
|
@@ -777,7 +805,8 @@ export function createRuntime(config) {
|
|
|
777
805
|
// append the provider's compiled-index block to the instructions. A
|
|
778
806
|
// provider failure degrades to a turn without memory, never a dead turn.
|
|
779
807
|
let instructions = cfg.instructions;
|
|
780
|
-
|
|
808
|
+
const memoryOn = args.memory ?? true;
|
|
809
|
+
if (memoryOn && cfg.memory && config.memory) {
|
|
781
810
|
const memArgs = { principal, agent: run.agent, memory: cfg.memory };
|
|
782
811
|
try {
|
|
783
812
|
// Resolve both hooks BEFORE mutating the toolset, so a failure in
|
|
@@ -890,7 +919,13 @@ export function createRuntime(config) {
|
|
|
890
919
|
depth: depth + 1,
|
|
891
920
|
autoApprove,
|
|
892
921
|
blockGated,
|
|
922
|
+
// Memory off stays off down the tree: an eval turn's delegate must
|
|
923
|
+
// not recall earlier inputs or save new memory either.
|
|
924
|
+
...(memoryOn ? {} : { memory: false }),
|
|
893
925
|
onProgress,
|
|
926
|
+
// Who is delegating — persisted on the sub-thread so every later
|
|
927
|
+
// resolve of it can be shaped per parent (DynamicAgentResolveContext).
|
|
928
|
+
parentAgent: run.agent,
|
|
894
929
|
// Telemetry linkage: this turn is the parent; carry the root chat
|
|
895
930
|
// down so the subagent's trace nests under this conversation.
|
|
896
931
|
parentRunId: run.id,
|
|
@@ -963,18 +998,36 @@ export function createRuntime(config) {
|
|
|
963
998
|
// Test runs: stub the approval-gated (mutating) tools so a trial has no
|
|
964
999
|
// real side effects. Gated set = connection defaults + the agent's policy.
|
|
965
1000
|
if (blockGated) {
|
|
1001
|
+
/**
|
|
1002
|
+
* Does this approval value actually gate the tool?
|
|
1003
|
+
*
|
|
1004
|
+
* NOT a truthiness check. The SDK spells a waiver as the STRING
|
|
1005
|
+
* `"not-applicable"` — which is truthy — so `if (v)` treated every
|
|
1006
|
+
* tool an agent had explicitly waived as gated and stubbed it. On an
|
|
1007
|
+
* agent whose reads are waived to override a connection default, that
|
|
1008
|
+
* is every read tool: the agent could not retrieve anything, and an
|
|
1009
|
+
* eval run measured a model with no access to its own corpus.
|
|
1010
|
+
*
|
|
1011
|
+
* Gated: `true`, `"user-approval"`, an object (approval with a
|
|
1012
|
+
* reason), a function. Waived: `false`, `undefined`,
|
|
1013
|
+
* `"not-applicable"`.
|
|
1014
|
+
*/
|
|
1015
|
+
const gatedBy = (v) => v === true ||
|
|
1016
|
+
v === "user-approval" ||
|
|
1017
|
+
typeof v === "function" ||
|
|
1018
|
+
(typeof v === "object" && v !== null);
|
|
966
1019
|
const gated = new Set();
|
|
967
1020
|
for (const [k, v] of Object.entries(harnessApproval))
|
|
968
|
-
if (v)
|
|
1021
|
+
if (gatedBy(v))
|
|
969
1022
|
gated.add(k);
|
|
970
1023
|
for (const o of opened) {
|
|
971
1024
|
for (const [k, v] of Object.entries(o.toolApproval ?? {}))
|
|
972
|
-
if (v)
|
|
1025
|
+
if (gatedBy(v))
|
|
973
1026
|
gated.add(k);
|
|
974
1027
|
}
|
|
975
1028
|
if (cfg.toolApproval && typeof cfg.toolApproval !== "function") {
|
|
976
1029
|
for (const [k, v] of Object.entries(cfg.toolApproval)) {
|
|
977
|
-
if (v)
|
|
1030
|
+
if (gatedBy(v))
|
|
978
1031
|
gated.add(k);
|
|
979
1032
|
else
|
|
980
1033
|
gated.delete(k);
|
|
@@ -992,12 +1045,19 @@ export function createRuntime(config) {
|
|
|
992
1045
|
if (t && typeof t.execute === "function") {
|
|
993
1046
|
tools[name] = {
|
|
994
1047
|
...t,
|
|
995
|
-
execute: async (input) =>
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1048
|
+
execute: async (input) => blockGated === "simulate-success"
|
|
1049
|
+
? {
|
|
1050
|
+
ok: true,
|
|
1051
|
+
__stubbed: true,
|
|
1052
|
+
tool: name,
|
|
1053
|
+
input,
|
|
1054
|
+
}
|
|
1055
|
+
: {
|
|
1056
|
+
__blocked: true,
|
|
1057
|
+
tool: name,
|
|
1058
|
+
input,
|
|
1059
|
+
reason: "approval-gated tool blocked during test run (no real side effects)",
|
|
1060
|
+
},
|
|
1001
1061
|
};
|
|
1002
1062
|
}
|
|
1003
1063
|
}
|
|
@@ -1130,7 +1190,7 @@ export function createRuntime(config) {
|
|
|
1130
1190
|
: config.resolvePromptCaching
|
|
1131
1191
|
? config.resolvePromptCaching(modelId, {
|
|
1132
1192
|
agent: run.agent,
|
|
1133
|
-
memoryScoped: !!(cfg.memory && config.memory),
|
|
1193
|
+
memoryScoped: !!(memoryOn && cfg.memory && config.memory),
|
|
1134
1194
|
principal,
|
|
1135
1195
|
})
|
|
1136
1196
|
: // No hook → caching is ON by default for first-party Anthropic /
|
|
@@ -1860,7 +1920,10 @@ export function createRuntime(config) {
|
|
|
1860
1920
|
});
|
|
1861
1921
|
}
|
|
1862
1922
|
const resolvedChatId = chat.id;
|
|
1863
|
-
const { cfg, modelId, runtimeCtx } = await resolveAgentConfig(agentName, principal, request, turnContext
|
|
1923
|
+
const { cfg, modelId, runtimeCtx } = await resolveAgentConfig(agentName, principal, request, turnContext,
|
|
1924
|
+
// A host may continue a subagent thread through the chat surface; keep
|
|
1925
|
+
// resolving it the way its spawner would.
|
|
1926
|
+
{ parentAgent: chat.parentAgent });
|
|
1864
1927
|
const prior = await storage.loadMessages(principal, resolvedChatId);
|
|
1865
1928
|
// A new message while the last turn is parked on an approval is itself the
|
|
1866
1929
|
// decision: the user declined to answer and wants to steer elsewhere. Close
|
|
@@ -1902,11 +1965,38 @@ export function createRuntime(config) {
|
|
|
1902
1965
|
return null;
|
|
1903
1966
|
// System op (no request): rebuild the principal from the run's identity blob.
|
|
1904
1967
|
const principal = await reconstructPrincipal(run.identity);
|
|
1905
|
-
const uiMessages = await storage.loadMessages(principal, run.chatId);
|
|
1906
1968
|
const lease = {
|
|
1907
1969
|
owner: dur.instanceId,
|
|
1908
1970
|
fencingToken: run.fencingToken ?? 1,
|
|
1909
1971
|
};
|
|
1972
|
+
if (principal === null) {
|
|
1973
|
+
// Refused (see `ReconstructPrincipal`): the identity may no longer act, so
|
|
1974
|
+
// nothing below — not even reading its history — runs as it. End the run
|
|
1975
|
+
// instead of returning early with the lease held: an `active` row would be
|
|
1976
|
+
// reaped and re-refused every tick until `maxAttempts`, and a throw here
|
|
1977
|
+
// would abort the rest of `sweep`'s loop. No `onRunFinished`: its meta
|
|
1978
|
+
// carries the principal, and there is none to report.
|
|
1979
|
+
const error = "principal rejected: reconstructPrincipal refused the run's identity";
|
|
1980
|
+
await storage.updateRun(run.id, {
|
|
1981
|
+
status: "errored",
|
|
1982
|
+
error,
|
|
1983
|
+
endedAt: Date.now(),
|
|
1984
|
+
// Terminal, so give up the reclaim just taken. `reclaimRun` matches
|
|
1985
|
+
// any non-completed row whose lease has lapsed, so a lease left to
|
|
1986
|
+
// expire would let a direct `resume()` re-claim and re-refuse it,
|
|
1987
|
+
// spending an attempt each time.
|
|
1988
|
+
leaseOwner: null,
|
|
1989
|
+
leaseExpiresAt: null,
|
|
1990
|
+
}, lease);
|
|
1991
|
+
console.warn(`[resume] run ${run.id} (${run.agent}): ${error}`);
|
|
1992
|
+
const settled = await storage.getRun(runId);
|
|
1993
|
+
return {
|
|
1994
|
+
runId,
|
|
1995
|
+
status: settled?.status ?? "errored",
|
|
1996
|
+
attempt: settled?.attempt ?? run.attempt ?? null,
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
const uiMessages = await storage.loadMessages(principal, run.chatId);
|
|
1910
2000
|
// What does this run actually need? A crash between the last step's
|
|
1911
2001
|
// checkpoint and `onFinish`'s `updateRun` leaves a FINISHED turn on an
|
|
1912
2002
|
// `active` row, and re-running the model there would pay twice for an answer
|
|
@@ -2001,7 +2091,12 @@ export function createRuntime(config) {
|
|
|
2001
2091
|
// reconcilable even if its (dynamic, DB-stored) agent was since deleted.
|
|
2002
2092
|
// Before this ordering that throw also aborted `sweep`'s loop, so one such
|
|
2003
2093
|
// run held up recovery for every other run until its attempts ran out.
|
|
2004
|
-
|
|
2094
|
+
// A recovered subagent run must resolve exactly as its spawner shaped it —
|
|
2095
|
+
// crash recovery and `sweep` both land here.
|
|
2096
|
+
const chat = await storage.getChat(principal, run.chatId);
|
|
2097
|
+
const { cfg, modelId, runtimeCtx } = await resolveAgentConfig(run.agent, principal, undefined, undefined, {
|
|
2098
|
+
parentAgent: chat?.parentAgent,
|
|
2099
|
+
});
|
|
2005
2100
|
// A resume was admitted when the turn first ran — never refuse it (the
|
|
2006
2101
|
// half-done work would strand), but its tokens still settle against the
|
|
2007
2102
|
// same counters, and the mid-turn stop still applies.
|
|
@@ -2046,16 +2141,31 @@ export function createRuntime(config) {
|
|
|
2046
2141
|
// Give up on poison runs that keep dying — don't loop forever.
|
|
2047
2142
|
if ((run.attempt ?? 1) >= maxAttempts)
|
|
2048
2143
|
continue;
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2144
|
+
// One run's failure must not strand the rest of the batch. The reaper
|
|
2145
|
+
// flipped EVERY run here to `errored` in one write, and no later reap
|
|
2146
|
+
// selects an `errored` row — so an escaping throw (a transient
|
|
2147
|
+
// `reconstructPrincipal` failure, a storage blip) would leave each run
|
|
2148
|
+
// after it unrecovered for good. A throw AFTER the reclaim (the hook,
|
|
2149
|
+
// history, the turn) leaves that run `active` under a lease, so it is
|
|
2150
|
+
// reaped and retried once the lease lapses, spending an attempt like any
|
|
2151
|
+
// other failed resume. A throw FROM the reclaim itself leaves the run as
|
|
2152
|
+
// the reaper left it — `errored`, not auto-retried — which is today's
|
|
2153
|
+
// outcome for that run too; only its neighbours are new to recovery.
|
|
2154
|
+
try {
|
|
2155
|
+
const outcome = await resumeRun(run.id);
|
|
2156
|
+
if (outcome)
|
|
2157
|
+
resumed.push(outcome);
|
|
2158
|
+
}
|
|
2159
|
+
catch (e) {
|
|
2160
|
+
console.error(`[resume] run ${run.id} (${run.agent}) failed; continuing the sweep:`, e);
|
|
2161
|
+
}
|
|
2052
2162
|
}
|
|
2053
2163
|
return { reaped, resumed };
|
|
2054
2164
|
}
|
|
2055
2165
|
async function cron(opts) {
|
|
2056
2166
|
const errors = [];
|
|
2057
2167
|
const describe = (e) => (e instanceof Error ? e.message : String(e));
|
|
2058
|
-
let due = { fired: 0, errors: 0, skipped: 0, parked: 0 };
|
|
2168
|
+
let due = { fired: 0, errors: 0, skipped: 0, parked: 0, rejected: 0 };
|
|
2059
2169
|
try {
|
|
2060
2170
|
due = await runDue({ now: opts?.now });
|
|
2061
2171
|
}
|
|
@@ -2150,7 +2260,7 @@ export function createRuntime(config) {
|
|
|
2150
2260
|
if (hasPendingApproval(messages)) {
|
|
2151
2261
|
return new Response(null, { status: 200 });
|
|
2152
2262
|
}
|
|
2153
|
-
const { cfg, modelId, runtimeCtx } = await resolveAgentConfig(chat.agent, principal, request, turnContext);
|
|
2263
|
+
const { cfg, modelId, runtimeCtx } = await resolveAgentConfig(chat.agent, principal, request, turnContext, { parentAgent: chat.parentAgent });
|
|
2154
2264
|
const admitted = await admitContinuation(principal, chat.agent, "decision");
|
|
2155
2265
|
const { run, lease } = await openRun(principal, chatId, chat.agent, cfg.configVersion);
|
|
2156
2266
|
return buildTurn({
|
|
@@ -2198,7 +2308,7 @@ export function createRuntime(config) {
|
|
|
2198
2308
|
await storage.appendMessages(chatId, changed);
|
|
2199
2309
|
return new Response(null, { status: 200 });
|
|
2200
2310
|
}
|
|
2201
|
-
const { cfg, modelId, runtimeCtx } = await resolveAgentConfig(chat.agent, principal, request, turnContext);
|
|
2311
|
+
const { cfg, modelId, runtimeCtx } = await resolveAgentConfig(chat.agent, principal, request, turnContext, { parentAgent: chat.parentAgent });
|
|
2202
2312
|
const admitted = await admitContinuation(principal, chat.agent, "decision");
|
|
2203
2313
|
// The result is persisted WITH the run (atomically where the adapter can):
|
|
2204
2314
|
// a busy chat refuses before anything is recorded, so the retry finds the
|
|
@@ -2244,7 +2354,7 @@ export function createRuntime(config) {
|
|
|
2244
2354
|
if (hasPendingApproval(live) || hasPendingToolResult([live[live.length - 1]])) {
|
|
2245
2355
|
throw new PendingApprovalError("This chat is awaiting a decision — resolve the pending request before compacting.");
|
|
2246
2356
|
}
|
|
2247
|
-
const { cfg, modelId } = await resolveAgentConfig(chat.agent, principal, request, turnContext);
|
|
2357
|
+
const { cfg, modelId } = await resolveAgentConfig(chat.agent, principal, request, turnContext, { parentAgent: chat.parentAgent });
|
|
2248
2358
|
// Tool definitions only affect how a recorded tool RESULT is rendered for the
|
|
2249
2359
|
// model (`toModelOutput`); an unknown tool falls back to its raw JSON rather
|
|
2250
2360
|
// than failing. So take every definition available without I/O — the agent's
|
|
@@ -2377,11 +2487,14 @@ export function createRuntime(config) {
|
|
|
2377
2487
|
return chat ? run : null;
|
|
2378
2488
|
}
|
|
2379
2489
|
/** A fresh user message from plain text (for cron-fired turns). */
|
|
2380
|
-
function userMessageOf(text) {
|
|
2490
|
+
function userMessageOf(text, files) {
|
|
2381
2491
|
return {
|
|
2382
2492
|
id: generateMessageId(),
|
|
2383
2493
|
role: "user",
|
|
2384
|
-
parts: [
|
|
2494
|
+
parts: [
|
|
2495
|
+
...(files ?? []).map((f) => ({ type: "file", url: f.url, mediaType: f.mediaType, ...(f.filename ? { filename: f.filename } : {}) })),
|
|
2496
|
+
{ type: "text", text },
|
|
2497
|
+
],
|
|
2385
2498
|
metadata: { visibility: "user", createdAt: Date.now() },
|
|
2386
2499
|
};
|
|
2387
2500
|
}
|
|
@@ -2443,8 +2556,15 @@ export function createRuntime(config) {
|
|
|
2443
2556
|
// Runtime-created thread: stamp it internal so a host filtering its
|
|
2444
2557
|
// chat list on kind never shows it (see ChatRecord.kind).
|
|
2445
2558
|
kind: "internal",
|
|
2559
|
+
...(o.parentAgent ? { parentAgent: o.parentAgent } : {}),
|
|
2446
2560
|
});
|
|
2447
2561
|
}
|
|
2562
|
+
// A spawn may only continue a thread its own agent delegated. Otherwise
|
|
2563
|
+
// agent B could pass agent A's sub-thread id and have the child resolved
|
|
2564
|
+
// under A — borrowing whatever a host grants A's subagents.
|
|
2565
|
+
if (o.parentAgent && chat.parentAgent && chat.parentAgent !== o.parentAgent) {
|
|
2566
|
+
throw new Error(`Thread ${chatId} was delegated by another agent; omit threadId to start a new one.`);
|
|
2567
|
+
}
|
|
2448
2568
|
const prior = await storage.loadMessages(o.principal, chatId);
|
|
2449
2569
|
// Same rule as handleChat: a new message while the thread is parked on an
|
|
2450
2570
|
// approval IS the decision — close the stale gate as declined (persisted)
|
|
@@ -2461,7 +2581,14 @@ export function createRuntime(config) {
|
|
|
2461
2581
|
// the run first, persist the message only once it is ours to run.
|
|
2462
2582
|
const admitted = await admitTurn(o.principal, o.agent, o.trigger);
|
|
2463
2583
|
const { cfg, modelId, runtimeCtx, run, lease } = await withReservation(admitted, async () => {
|
|
2464
|
-
const resolved = await resolveAgentConfig(o.agent, o.principal, undefined, o.turnContext
|
|
2584
|
+
const resolved = await resolveAgentConfig(o.agent, o.principal, undefined, o.turnContext, {
|
|
2585
|
+
// A continued thread keeps the parent it was created under (a mismatch
|
|
2586
|
+
// was refused above). Without a stored one — an adapter that doesn't
|
|
2587
|
+
// persist the column — the spawner of THIS turn is the parent: a host
|
|
2588
|
+
// that restricts a subagent by its parent must never see none.
|
|
2589
|
+
parentAgent: chat.parentAgent ?? o.parentAgent,
|
|
2590
|
+
autoApprove: o.autoApprove,
|
|
2591
|
+
});
|
|
2465
2592
|
const opened = await openTurn(o.principal, chatId, o.agent, [o.message], resolved.cfg.configVersion);
|
|
2466
2593
|
return { ...resolved, ...opened };
|
|
2467
2594
|
});
|
|
@@ -2478,6 +2605,8 @@ export function createRuntime(config) {
|
|
|
2478
2605
|
admitted,
|
|
2479
2606
|
autoApprove: o.autoApprove,
|
|
2480
2607
|
blockGated: o.blockGated,
|
|
2608
|
+
memory: o.memory,
|
|
2609
|
+
model: o.model,
|
|
2481
2610
|
promptCaching: o.promptCaching,
|
|
2482
2611
|
depth: o.depth ?? 0,
|
|
2483
2612
|
parentRunId: o.parentRunId,
|
|
@@ -2506,7 +2635,9 @@ export function createRuntime(config) {
|
|
|
2506
2635
|
}
|
|
2507
2636
|
: undefined,
|
|
2508
2637
|
});
|
|
2509
|
-
|
|
2638
|
+
// The turn's own failure travels as an `error` chunk — the stream, not a
|
|
2639
|
+
// throw. Keep its text: without it a failed turn reads as a silent one.
|
|
2640
|
+
const error = lastStreamError(await res.text());
|
|
2510
2641
|
const after = await storage.loadMessages(o.principal, chatId);
|
|
2511
2642
|
// The verdict is about THIS turn, so read the FINAL message only (the same
|
|
2512
2643
|
// rule hasPendingApproval applies): an undecided gate in an older message
|
|
@@ -2521,6 +2652,7 @@ export function createRuntime(config) {
|
|
|
2521
2652
|
answer: pending.length > 0 ? "" : lastAssistantText(after),
|
|
2522
2653
|
parked,
|
|
2523
2654
|
...(pending.length > 0 ? { pending } : {}),
|
|
2655
|
+
...(error ? { error } : {}),
|
|
2524
2656
|
};
|
|
2525
2657
|
}
|
|
2526
2658
|
/**
|
|
@@ -2546,7 +2678,10 @@ export function createRuntime(config) {
|
|
|
2546
2678
|
// Another gated call in the same sub step still undecided → can't finish.
|
|
2547
2679
|
if (hasPendingApproval(subMessages))
|
|
2548
2680
|
return { answer: "" };
|
|
2549
|
-
const { cfg, modelId, runtimeCtx } = await resolveAgentConfig(subChat.agent, principal
|
|
2681
|
+
const { cfg, modelId, runtimeCtx } = await resolveAgentConfig(subChat.agent, principal, undefined, undefined,
|
|
2682
|
+
// Same shaping as the first sub-turn: the parent is read off the persisted
|
|
2683
|
+
// sub-thread, and this path is only ever reached by a human decision.
|
|
2684
|
+
{ parentAgent: subChat.parentAgent, autoApprove: false });
|
|
2550
2685
|
const admitted = await admitContinuation(principal, subChat.agent, "decision");
|
|
2551
2686
|
const { run, lease } = await openRun(principal, subChatId, subChat.agent, cfg.configVersion);
|
|
2552
2687
|
const res = await buildTurn({
|
|
@@ -2609,8 +2744,28 @@ export function createRuntime(config) {
|
|
|
2609
2744
|
let errors = 0;
|
|
2610
2745
|
let skipped = 0;
|
|
2611
2746
|
let parked = 0;
|
|
2747
|
+
let rejected = 0;
|
|
2612
2748
|
for (const s of claimed) {
|
|
2613
2749
|
const next = s.cron ? (config.cron?.(s.cron, now, s.timezone) ?? null) : null;
|
|
2750
|
+
// Rebuild the principal FIRST, because a refusal has to reach the one
|
|
2751
|
+
// write below: `completeSchedule` is fenced by the claim lease and clears
|
|
2752
|
+
// it, so there is no second write in which to disable the schedule. A
|
|
2753
|
+
// refused identity (`null`) is permanent — the owner was removed, the
|
|
2754
|
+
// account is gone — so the schedule is disabled rather than left to refuse
|
|
2755
|
+
// on every tick, which is how a revoked user's schedule stays invisible
|
|
2756
|
+
// forever. A THROW is transient and keeps today's contract: the occurrence
|
|
2757
|
+
// is still consumed, the fire is counted in `errors`, the schedule stays
|
|
2758
|
+
// live. Hosts keep the hook fast (see `reconstructPrincipal`); it now runs
|
|
2759
|
+
// inside the claim window.
|
|
2760
|
+
let principal = null;
|
|
2761
|
+
let principalError;
|
|
2762
|
+
try {
|
|
2763
|
+
principal = await reconstructPrincipal(s.identity);
|
|
2764
|
+
}
|
|
2765
|
+
catch (error) {
|
|
2766
|
+
principalError = { error };
|
|
2767
|
+
}
|
|
2768
|
+
const refused = !principalError && principal === null;
|
|
2614
2769
|
// Consume the occurrence BEFORE firing, not after. The claim lease is
|
|
2615
2770
|
// stamped once (never heartbeated), so while a fire is in flight the row
|
|
2616
2771
|
// still reads `enabled AND nextRunAt <= now` — and once the lease TTL
|
|
@@ -2625,7 +2780,20 @@ export function createRuntime(config) {
|
|
|
2625
2780
|
// survives and `sweep()` resumes it. For work that is billed per
|
|
2626
2781
|
// attempt, "ran twice" is strictly worse than "ran once, resumed".
|
|
2627
2782
|
try {
|
|
2628
|
-
await storage.completeSchedule(s.id, { nextRunAt: next ?? now, lastRunAt: now, enabled: next != null }, dur.instanceId
|
|
2783
|
+
const applied = await storage.completeSchedule(s.id, { nextRunAt: next ?? now, lastRunAt: now, enabled: next != null && !refused }, dur.instanceId,
|
|
2784
|
+
// Which claim this is: a later pass on this same instance may have
|
|
2785
|
+
// re-claimed the row under the same owner id since.
|
|
2786
|
+
s.leaseExpiresAt != null ? { leaseExpiresAt: s.leaseExpiresAt } : undefined);
|
|
2787
|
+
if (applied === false) {
|
|
2788
|
+
// The fence rejected us: our claim lapsed (the lease is never
|
|
2789
|
+
// renewed, and the hook above runs inside it) and a newer claim —
|
|
2790
|
+
// another node's, or a later pass on this one — holds this
|
|
2791
|
+
// occurrence. It fires it and accounts for it; firing here too would
|
|
2792
|
+
// be the duplicate paid turn consuming-first exists to prevent.
|
|
2793
|
+
console.warn(`[schedule] ${s.id} (${s.agent}): claim lost before the occurrence was consumed; ` +
|
|
2794
|
+
`the newer claim owns this fire`);
|
|
2795
|
+
continue;
|
|
2796
|
+
}
|
|
2629
2797
|
}
|
|
2630
2798
|
catch (e) {
|
|
2631
2799
|
// Cannot consume the occurrence → do NOT fire. Firing anyway would
|
|
@@ -2635,8 +2803,18 @@ export function createRuntime(config) {
|
|
|
2635
2803
|
errors++;
|
|
2636
2804
|
continue;
|
|
2637
2805
|
}
|
|
2806
|
+
if (principalError) {
|
|
2807
|
+
console.error(`[schedule] fire failed for ${s.id} (${s.agent}): reconstructPrincipal threw:`, principalError.error);
|
|
2808
|
+
errors++;
|
|
2809
|
+
continue;
|
|
2810
|
+
}
|
|
2811
|
+
if (principal === null) {
|
|
2812
|
+
console.warn(`[schedule] ${s.id} (${s.agent}): reconstructPrincipal refused the schedule's ` +
|
|
2813
|
+
`identity — not fired, schedule disabled`);
|
|
2814
|
+
rejected++;
|
|
2815
|
+
continue;
|
|
2816
|
+
}
|
|
2638
2817
|
try {
|
|
2639
|
-
const principal = await reconstructPrincipal(s.identity);
|
|
2640
2818
|
// Parked-predecessor guard: when the LAST fire's chat is still waiting
|
|
2641
2819
|
// on a human (an undecided approval or tool result), firing again just
|
|
2642
2820
|
// mints another parked chat and pays for the tokens up to the gate —
|
|
@@ -2735,29 +2913,31 @@ export function createRuntime(config) {
|
|
|
2735
2913
|
errors++;
|
|
2736
2914
|
}
|
|
2737
2915
|
}
|
|
2738
|
-
return { fired, errors, skipped, parked };
|
|
2916
|
+
return { fired, errors, skipped, parked, rejected };
|
|
2739
2917
|
}
|
|
2740
2918
|
const self = {
|
|
2741
2919
|
listAgents: agentInfos,
|
|
2742
2920
|
handleChat,
|
|
2743
|
-
runAgent: async ({ principal, agent, prompt, threadId, blockGated = true, turnContext, promptCaching,
|
|
2921
|
+
runAgent: async ({ principal, agent, prompt, files, threadId, blockGated = true, memory, model, turnContext, promptCaching,
|
|
2744
2922
|
// No client stream: assume unattended unless the host says otherwise, so
|
|
2745
2923
|
// a park here can't be silently filtered out as "someone's watching".
|
|
2746
2924
|
trigger = "programmatic", }) => {
|
|
2747
2925
|
const r = await runToCompletion({
|
|
2748
2926
|
principal,
|
|
2749
2927
|
agent,
|
|
2750
|
-
message: userMessageOf(prompt),
|
|
2928
|
+
message: userMessageOf(prompt, files),
|
|
2751
2929
|
chatId: threadId,
|
|
2752
2930
|
depth: 1,
|
|
2753
2931
|
// Autonomous: no human to authorize (blockGated stubs the gated tools).
|
|
2754
2932
|
autoApprove: true,
|
|
2755
2933
|
blockGated,
|
|
2934
|
+
memory,
|
|
2935
|
+
model,
|
|
2756
2936
|
turnContext,
|
|
2757
2937
|
promptCaching,
|
|
2758
2938
|
trigger,
|
|
2759
2939
|
});
|
|
2760
|
-
return { threadId: r.chatId, answer: r.answer };
|
|
2940
|
+
return { threadId: r.chatId, answer: r.answer, ...(r.error ? { error: r.error } : {}) };
|
|
2761
2941
|
},
|
|
2762
2942
|
compactChat,
|
|
2763
2943
|
loadHistory,
|