@earendil-works/pi-durable 1.3.0 → 1.5.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/CHANGELOG.md +227 -0
- package/README.md +287 -82
- package/dist/codemode/codemode.d.ts +4 -4
- package/dist/codemode/codemode.d.ts.map +1 -1
- package/dist/codemode/codemode.js +21 -16
- package/dist/codemode/codemode.js.map +1 -1
- package/dist/codemode/models.d.ts.map +1 -1
- package/dist/codemode/models.js +6 -4
- package/dist/codemode/models.js.map +1 -1
- package/dist/documents.d.ts +1 -1
- package/dist/documents.d.ts.map +1 -1
- package/dist/documents.js.map +1 -1
- package/dist/env/index.d.ts +12 -10
- package/dist/env/index.d.ts.map +1 -1
- package/dist/env/index.js.map +1 -1
- package/dist/env/node-watch.d.ts +3 -3
- package/dist/env/node-watch.d.ts.map +1 -1
- package/dist/env/node-watch.js +18 -16
- package/dist/env/node-watch.js.map +1 -1
- package/dist/env/node.d.ts +2 -2
- package/dist/env/node.d.ts.map +1 -1
- package/dist/env/node.js +12 -4
- package/dist/env/node.js.map +1 -1
- package/dist/errors.d.ts +18 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +26 -3
- package/dist/errors.js.map +1 -1
- package/dist/harness/agent.d.ts +13 -2
- package/dist/harness/agent.d.ts.map +1 -1
- package/dist/harness/agent.js +48 -6
- package/dist/harness/agent.js.map +1 -1
- package/dist/harness/compaction.d.ts +3 -1
- package/dist/harness/compaction.d.ts.map +1 -1
- package/dist/harness/compaction.js +38 -6
- package/dist/harness/compaction.js.map +1 -1
- package/dist/harness/context.d.ts +23 -7
- package/dist/harness/context.d.ts.map +1 -1
- package/dist/harness/context.js +71 -25
- package/dist/harness/context.js.map +1 -1
- package/dist/harness/define.d.ts +7 -3
- package/dist/harness/define.d.ts.map +1 -1
- package/dist/harness/define.js +4 -0
- package/dist/harness/define.js.map +1 -1
- package/dist/harness/generation.d.ts +38 -5
- package/dist/harness/generation.d.ts.map +1 -1
- package/dist/harness/generation.js +497 -108
- package/dist/harness/generation.js.map +1 -1
- package/dist/harness/harness.d.ts +7 -2
- package/dist/harness/harness.d.ts.map +1 -1
- package/dist/harness/harness.js +147 -57
- package/dist/harness/harness.js.map +1 -1
- package/dist/harness/inbox.d.ts +7 -3
- package/dist/harness/inbox.d.ts.map +1 -1
- package/dist/harness/inbox.js +7 -4
- package/dist/harness/inbox.js.map +1 -1
- package/dist/harness/json.d.ts +20 -4
- package/dist/harness/json.d.ts.map +1 -1
- package/dist/harness/json.js +23 -5
- package/dist/harness/json.js.map +1 -1
- package/dist/harness/live.d.ts +122 -16
- package/dist/harness/live.d.ts.map +1 -1
- package/dist/harness/live.js +93 -12
- package/dist/harness/live.js.map +1 -1
- package/dist/harness/partial-changes.d.ts +54 -0
- package/dist/harness/partial-changes.d.ts.map +1 -0
- package/dist/harness/partial-changes.js +162 -0
- package/dist/harness/partial-changes.js.map +1 -0
- package/dist/harness/prompt.d.ts +1 -1
- package/dist/harness/prompt.d.ts.map +1 -1
- package/dist/harness/prompt.js +1 -1
- package/dist/harness/prompt.js.map +1 -1
- package/dist/harness/registry-reader.d.ts +11 -0
- package/dist/harness/registry-reader.d.ts.map +1 -0
- package/dist/harness/registry-reader.js +25 -0
- package/dist/harness/registry-reader.js.map +1 -0
- package/dist/harness/scheduler.d.ts +86 -14
- package/dist/harness/scheduler.d.ts.map +1 -1
- package/dist/harness/scheduler.js +1459 -369
- package/dist/harness/scheduler.js.map +1 -1
- package/dist/harness/submissions.d.ts +23 -11
- package/dist/harness/submissions.d.ts.map +1 -1
- package/dist/harness/submissions.js +104 -36
- package/dist/harness/submissions.js.map +1 -1
- package/dist/harness/task-graph.d.ts +28 -8
- package/dist/harness/task-graph.d.ts.map +1 -1
- package/dist/harness/task-graph.js +85 -36
- package/dist/harness/task-graph.js.map +1 -1
- package/dist/harness/telemetry.d.ts +307 -0
- package/dist/harness/telemetry.d.ts.map +1 -0
- package/dist/harness/telemetry.js +136 -0
- package/dist/harness/telemetry.js.map +1 -0
- package/dist/harness/tool.d.ts +2 -0
- package/dist/harness/tool.d.ts.map +1 -1
- package/dist/harness/tool.js +223 -61
- package/dist/harness/tool.js.map +1 -1
- package/dist/harness/transcript.d.ts +72 -0
- package/dist/harness/transcript.d.ts.map +1 -0
- package/dist/harness/transcript.js +264 -0
- package/dist/harness/transcript.js.map +1 -0
- package/dist/harness/types.d.ts +302 -48
- package/dist/harness/types.d.ts.map +1 -1
- package/dist/harness/types.js.map +1 -1
- package/dist/harness/util.d.ts +7 -1
- package/dist/harness/util.d.ts.map +1 -1
- package/dist/harness/util.js +13 -0
- package/dist/harness/util.js.map +1 -1
- package/dist/harness/view.d.ts +43 -22
- package/dist/harness/view.d.ts.map +1 -1
- package/dist/harness/view.js +130 -71
- package/dist/harness/view.js.map +1 -1
- package/dist/index.d.ts +13 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/search.d.ts +19 -3
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +107 -44
- package/dist/search.js.map +1 -1
- package/dist/session/cached-storage.d.ts +71 -0
- package/dist/session/cached-storage.d.ts.map +1 -0
- package/dist/session/cached-storage.js +269 -0
- package/dist/session/cached-storage.js.map +1 -0
- package/dist/session/conversation-tails.d.ts +33 -0
- package/dist/session/conversation-tails.d.ts.map +1 -0
- package/dist/session/conversation-tails.js +101 -0
- package/dist/session/conversation-tails.js.map +1 -0
- package/dist/session/keys.d.ts +68 -0
- package/dist/session/keys.d.ts.map +1 -0
- package/dist/session/keys.js +161 -0
- package/dist/session/keys.js.map +1 -0
- package/dist/session/session.d.ts +181 -29
- package/dist/session/session.d.ts.map +1 -1
- package/dist/session/session.js +1073 -183
- package/dist/session/session.js.map +1 -1
- package/dist/session/transaction.d.ts +86 -4
- package/dist/session/transaction.d.ts.map +1 -1
- package/dist/session/transaction.js +250 -38
- package/dist/session/transaction.js.map +1 -1
- package/dist/session/writer.d.ts +74 -0
- package/dist/session/writer.d.ts.map +1 -0
- package/dist/session/writer.js +294 -0
- package/dist/session/writer.js.map +1 -0
- package/dist/storage/indexeddb.d.ts +68 -0
- package/dist/storage/indexeddb.d.ts.map +1 -0
- package/dist/storage/indexeddb.js +881 -0
- package/dist/storage/indexeddb.js.map +1 -0
- package/dist/storage/jsonl/storage.d.ts +30 -2
- package/dist/storage/jsonl/storage.d.ts.map +1 -1
- package/dist/storage/jsonl/storage.js +88 -24
- package/dist/storage/jsonl/storage.js.map +1 -1
- package/dist/storage/memory.d.ts +7 -2
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +29 -3
- package/dist/storage/memory.js.map +1 -1
- package/dist/storage/sqlite/cloudflare.d.ts +2 -51
- package/dist/storage/sqlite/cloudflare.d.ts.map +1 -1
- package/dist/storage/sqlite/cloudflare.js +2 -133
- package/dist/storage/sqlite/cloudflare.js.map +1 -1
- package/dist/storage/sqlite/database.d.ts +1 -36
- package/dist/storage/sqlite/database.d.ts.map +1 -1
- package/dist/storage/sqlite/database.js.map +1 -1
- package/dist/storage/sqlite/index.d.ts +1 -1
- package/dist/storage/sqlite/index.d.ts.map +1 -1
- package/dist/storage/sqlite/index.js.map +1 -1
- package/dist/storage/sqlite/migrations.d.ts +2 -5
- package/dist/storage/sqlite/migrations.d.ts.map +1 -1
- package/dist/storage/sqlite/migrations.js +28 -28
- package/dist/storage/sqlite/migrations.js.map +1 -1
- package/dist/storage/sqlite/node.d.ts +2 -37
- package/dist/storage/sqlite/node.d.ts.map +1 -1
- package/dist/storage/sqlite/node.js +3 -174
- package/dist/storage/sqlite/node.js.map +1 -1
- package/dist/storage/sqlite/schema.d.ts +1 -3
- package/dist/storage/sqlite/schema.d.ts.map +1 -1
- package/dist/storage/sqlite/schema.js +2 -9
- package/dist/storage/sqlite/schema.js.map +1 -1
- package/dist/storage/sqlite/storage.d.ts +15 -3
- package/dist/storage/sqlite/storage.d.ts.map +1 -1
- package/dist/storage/sqlite/storage.js +110 -36
- package/dist/storage/sqlite/storage.js.map +1 -1
- package/dist/storage/store/commit.d.ts +33 -0
- package/dist/storage/store/commit.d.ts.map +1 -0
- package/dist/storage/store/commit.js +286 -0
- package/dist/storage/store/commit.js.map +1 -0
- package/dist/storage/store/documents.d.ts +43 -0
- package/dist/storage/store/documents.d.ts.map +1 -0
- package/dist/storage/store/documents.js +102 -0
- package/dist/storage/store/documents.js.map +1 -0
- package/dist/storage/store/index.d.ts +2 -0
- package/dist/storage/store/index.d.ts.map +1 -0
- package/dist/storage/store/index.js +2 -0
- package/dist/storage/store/index.js.map +1 -0
- package/dist/storage/store/queries.d.ts +49 -0
- package/dist/storage/store/queries.d.ts.map +1 -0
- package/dist/storage/store/queries.js +93 -0
- package/dist/storage/store/queries.js.map +1 -0
- package/dist/storage/store/storage.d.ts +59 -0
- package/dist/storage/store/storage.d.ts.map +1 -0
- package/dist/storage/store/storage.js +438 -0
- package/dist/storage/store/storage.js.map +1 -0
- package/dist/storage/store/tables.d.ts +54 -0
- package/dist/storage/store/tables.d.ts.map +1 -0
- package/dist/storage/store/tables.js +108 -0
- package/dist/storage/store/tables.js.map +1 -0
- package/dist/testing/commit-writes.d.ts +5 -0
- package/dist/testing/commit-writes.d.ts.map +1 -0
- package/dist/testing/commit-writes.js +12 -0
- package/dist/testing/commit-writes.js.map +1 -0
- package/dist/testing/env-conformance.js +7 -7
- package/dist/testing/env-conformance.js.map +1 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +1 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/storage-benchmark.d.ts.map +1 -1
- package/dist/testing/storage-benchmark.js +18 -17
- package/dist/testing/storage-benchmark.js.map +1 -1
- package/dist/testing/storage-conformance.d.ts.map +1 -1
- package/dist/testing/storage-conformance.js +369 -67
- package/dist/testing/storage-conformance.js.map +1 -1
- package/dist/timers.d.ts +21 -0
- package/dist/timers.d.ts.map +1 -0
- package/dist/timers.js +72 -0
- package/dist/timers.js.map +1 -0
- package/dist/tool-search/tool-search.d.ts +6 -12
- package/dist/tool-search/tool-search.d.ts.map +1 -1
- package/dist/tool-search/tool-search.js +7 -7
- package/dist/tool-search/tool-search.js.map +1 -1
- package/dist/tools/bash.d.ts +12 -12
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +16 -15
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit.d.ts +7 -7
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +9 -9
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/index.d.ts +32 -32
- package/dist/tools/read.d.ts +6 -6
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +19 -8
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/write.d.ts +4 -4
- package/dist/tools/write.d.ts.map +1 -1
- package/dist/tools/write.js +5 -5
- package/dist/tools/write.js.map +1 -1
- package/dist/types.d.ts +331 -64
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +27 -10
- package/dist/harness/events.d.ts +0 -186
- package/dist/harness/events.d.ts.map +0 -1
- package/dist/harness/events.js +0 -373
- package/dist/harness/events.js.map +0 -1
- package/dist/session/observation.d.ts +0 -42
- package/dist/session/observation.d.ts.map +0 -1
- package/dist/session/observation.js +0 -281
- package/dist/session/observation.js.map +0 -1
|
@@ -4,15 +4,17 @@ import { isRetryableAssistantError, retryDelayMs } from "@earendil-works/pi-ai/u
|
|
|
4
4
|
import { getCurrentTools } from "@earendil-works/pi-ai/utils/transcript";
|
|
5
5
|
import { AssistantEntry, ResetEntry, SystemEntry, UserEntry } from "../entries.js";
|
|
6
6
|
import { defineTask } from "../tasks.js";
|
|
7
|
-
import { addTools } from "./agent.js";
|
|
8
|
-
import { createCompaction, estimateContext, selectCut } from "./compaction.js";
|
|
9
|
-
import { applyBoundary, prepareBoundary } from "./inbox.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
7
|
+
import { addTools, agentHooks, sectionExtension } from "./agent.js";
|
|
8
|
+
import { compactionPolicy, createCompaction, estimateContext, selectCut } from "./compaction.js";
|
|
9
|
+
import { applyBoundary, hasQueuedReset, prepareBoundary, withdrawQueuedInputs } from "./inbox.js";
|
|
10
|
+
import { cutOffCount, cutOffTooOften, endRun, LiveDoc, PROGRESS_COMMIT, salvageable, } from "./live.js";
|
|
11
|
+
import { PartialChanges } from "./partial-changes.js";
|
|
12
12
|
import { planSystemEntries, renderSections, replaySections } from "./prompt.js";
|
|
13
13
|
import { ensureProviderSessionId } from "./provider.js";
|
|
14
|
+
import { endedByCrashes } from "./scheduler.js";
|
|
14
15
|
import { appendToolResult, harnessError, ToolTask } from "./tool.js";
|
|
15
16
|
import { recordUsage } from "./usage.js";
|
|
17
|
+
import { optionalExtension } from "./util.js";
|
|
16
18
|
const DEFAULT_POLL_AFTER_MS = 5000;
|
|
17
19
|
/**
|
|
18
20
|
* Built-in generation task: prepares the positional system prompt and tool loadout, requests or polls the model,
|
|
@@ -20,8 +22,15 @@ const DEFAULT_POLL_AFTER_MS = 5000;
|
|
|
20
22
|
*/
|
|
21
23
|
export const GenerationTask = defineTask({
|
|
22
24
|
name: "pi.generation",
|
|
23
|
-
version:
|
|
25
|
+
version: 2,
|
|
24
26
|
initial: () => ({ phase: "prepare", attempt: 1 }),
|
|
27
|
+
// Version 2 leaves calls to tools not offered in a sequential round's `pending`, flagged `unoffered` in their slots.
|
|
28
|
+
// A version 1 Harness would start their tool tasks unflagged, resolving the name, so it leaves version 2 blocked
|
|
29
|
+
// instead. A version 1 checkpoint has no such calls and is kept as it is.
|
|
30
|
+
migrate: (input, checkpoint) => ({
|
|
31
|
+
input: input,
|
|
32
|
+
checkpoint: checkpoint,
|
|
33
|
+
}),
|
|
25
34
|
phases: {
|
|
26
35
|
/**
|
|
27
36
|
* Render the system prompt and tool loadout and append the positional `pi.system` entries they need, then move to
|
|
@@ -30,6 +39,8 @@ export const GenerationTask = defineTask({
|
|
|
30
39
|
* may land meanwhile.
|
|
31
40
|
*/
|
|
32
41
|
prepare: async (task, runtime, context) => {
|
|
42
|
+
if (cutOffTooOften(task, runtime))
|
|
43
|
+
return endCutOff(runtime, task, context);
|
|
33
44
|
const { conversationId } = runtime;
|
|
34
45
|
const agent = await runtime.agent(context);
|
|
35
46
|
const settings = runtime.settings;
|
|
@@ -53,8 +64,13 @@ export const GenerationTask = defineTask({
|
|
|
53
64
|
}
|
|
54
65
|
}
|
|
55
66
|
const view = await runtime.context(conversationId, context);
|
|
67
|
+
// The model owes an answer only to a context ending in a user message or tool results; system messages say
|
|
68
|
+
// nothing it answers. Otherwise, as for a continue after an answer or in an empty conversation, end the run
|
|
69
|
+
// before any request.
|
|
70
|
+
const last = view.messages.findLast((message) => message.role !== "system");
|
|
71
|
+
if (last?.role !== "user" && last?.role !== "toolResult")
|
|
72
|
+
return failNothingToAnswer(runtime, context);
|
|
56
73
|
const shown = replaySections(view.messages);
|
|
57
|
-
const report = (error) => runtime.report(error);
|
|
58
74
|
let env;
|
|
59
75
|
try {
|
|
60
76
|
env = await runtime.env(context);
|
|
@@ -62,13 +78,16 @@ export const GenerationTask = defineTask({
|
|
|
62
78
|
catch (error) {
|
|
63
79
|
if (context.abortSignal?.aborted)
|
|
64
80
|
throw error;
|
|
65
|
-
report(error);
|
|
81
|
+
runtime.report(error, { kind: "host", callback: "env" });
|
|
66
82
|
}
|
|
67
83
|
const input = { conversationId, agent, env, shown: Object.fromEntries(shown), read: runtime };
|
|
68
|
-
const desired = await renderSections(agent.sections, input, shown, report,
|
|
84
|
+
const desired = await renderSections(agent.sections, input, shown, (error, section) => runtime.report(error, {
|
|
85
|
+
kind: "extension",
|
|
86
|
+
...optionalExtension(sectionExtension(agent, section.key)),
|
|
87
|
+
}), context);
|
|
69
88
|
const entries = planSystemEntries(view, desired, agent.tools, runtime.now());
|
|
70
89
|
const threshold = compacted === undefined
|
|
71
|
-
? thresholdCompaction(view, entries, resolved.contextWindow, settings
|
|
90
|
+
? thresholdCompaction(view, entries, resolved.contextWindow, compactionPolicy(settings, model))
|
|
72
91
|
: undefined;
|
|
73
92
|
if (threshold === "blocking") {
|
|
74
93
|
// Compact first and prepare again; the transcript is unchanged until the compaction appends.
|
|
@@ -80,11 +99,10 @@ export const GenerationTask = defineTask({
|
|
|
80
99
|
return;
|
|
81
100
|
}
|
|
82
101
|
await runtime.commit(async (tx) => {
|
|
83
|
-
|
|
102
|
+
// The context ends in a message the model answers, so the conversation has an entry.
|
|
103
|
+
let cutoff = (await tx.latestEntryId(conversationId));
|
|
84
104
|
for (const entry of entries)
|
|
85
105
|
cutoff = (await tx.appendEntry(SystemEntry, conversationId, entry)).id;
|
|
86
|
-
if (cutoff === undefined)
|
|
87
|
-
throw new Error(`Conversation ${conversationId} has no entries to send`);
|
|
88
106
|
// Checked in this commit, so a compaction admitted during preparation counts.
|
|
89
107
|
if (threshold === "background" && (await tx.doc(LiveDoc, conversationId)).compactions === undefined) {
|
|
90
108
|
await createCompaction(tx, conversationId, { reason: "threshold" });
|
|
@@ -101,14 +119,22 @@ export const GenerationTask = defineTask({
|
|
|
101
119
|
}, context);
|
|
102
120
|
},
|
|
103
121
|
request: async (task, runtime, context) => {
|
|
122
|
+
// Charging the attempt may itself be what keeps failing, such as converting a huge partial: the limit ends it.
|
|
123
|
+
if (cutOffTooOften(task, runtime))
|
|
124
|
+
return endCutOff(runtime, task, context);
|
|
125
|
+
// The host stopped while this request was in flight: a failed attempt, not a resend (spec §8.3).
|
|
126
|
+
if (task.restarts !== undefined)
|
|
127
|
+
return interrupted(runtime, task.state.checkpoint, context);
|
|
104
128
|
const { attempt, compacted, model: ref, thinkingLevel, streamOptions, cutoff } = task.state.checkpoint;
|
|
105
129
|
const conversationId = runtime.conversationId;
|
|
130
|
+
// The attempt shown is presentation, stored like progress (`PROGRESS_COMMIT`); converting a partial left by an
|
|
131
|
+
// interrupted attempt appends an entry, which makes the commit immediate.
|
|
106
132
|
await runtime.commit(async (tx) => {
|
|
107
133
|
const live = await tx.doc(LiveDoc, conversationId);
|
|
108
134
|
await convertPartial(tx, live, conversationId);
|
|
109
135
|
live.generation = { attempt };
|
|
110
136
|
return undefined;
|
|
111
|
-
}, context);
|
|
137
|
+
}, context, PROGRESS_COMMIT);
|
|
112
138
|
const model = runtime.models.getModel(ref.provider, ref.modelId);
|
|
113
139
|
if (model === undefined)
|
|
114
140
|
return failNoModel(runtime, ref, context);
|
|
@@ -122,37 +148,62 @@ export const GenerationTask = defineTask({
|
|
|
122
148
|
const options = {
|
|
123
149
|
...streamOptions,
|
|
124
150
|
signal: runtime.signal,
|
|
151
|
+
telemetry: context.telemetry,
|
|
125
152
|
sessionId: await ensureProviderSessionId(runtime, context),
|
|
126
153
|
...(thinkingLevel === "off" ? {} : { reasoning: thinkingLevel }),
|
|
154
|
+
onPayload: (payload) => providerPayload(runtime, model, payload, context),
|
|
127
155
|
};
|
|
128
156
|
const message = await streamResponse(runtime, model, messages, options, attempt, context);
|
|
129
|
-
const request = { attempt, compacted, model: ref, cutoff, messages: view.messages };
|
|
157
|
+
const request = { attempt, compacted, model: ref, thinkingLevel, cutoff, messages: view.messages };
|
|
130
158
|
await classify(runtime, request, message, context);
|
|
131
159
|
},
|
|
160
|
+
/** After the backoff's sleep; a run before `until` (an early wake, or a record of an earlier version) sleeps again. */
|
|
132
161
|
retry: async (task, runtime, context) => {
|
|
133
|
-
const { attempt, compacted, until } = task.state.checkpoint;
|
|
134
|
-
|
|
162
|
+
const { attempt, compacted, until, poll } = task.state.checkpoint;
|
|
163
|
+
if (cutOffTooOften(task, runtime))
|
|
164
|
+
return endCutOff(runtime, task, context);
|
|
165
|
+
if (until > runtime.now())
|
|
166
|
+
return runtime.sleepUntil(until, task.state.checkpoint, context);
|
|
135
167
|
await runtime.commit(async (tx) => {
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
168
|
+
const live = await tx.doc(LiveDoc, runtime.conversationId);
|
|
169
|
+
const kept = compacted === undefined ? {} : { compacted };
|
|
170
|
+
// A poll the host died during polls its response again, as the next attempt.
|
|
171
|
+
if (poll !== undefined) {
|
|
172
|
+
const pollAt = runtime.now();
|
|
173
|
+
live.generation = { attempt: attempt + 1, deferred: { pollAt } };
|
|
174
|
+
return {
|
|
175
|
+
status: "running",
|
|
176
|
+
checkpoint: { phase: "poll", attempt: attempt + 1, ...kept, ...poll, pollAt },
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
live.generation = { attempt: attempt + 1 };
|
|
180
|
+
return { status: "running", checkpoint: { phase: "prepare", attempt: attempt + 1, ...kept } };
|
|
143
181
|
}, context);
|
|
144
182
|
},
|
|
183
|
+
/** After the sleep until `pollAt`; a run before it (an early wake, or a record of an earlier version) sleeps again. */
|
|
145
184
|
poll: async (task, runtime, context) => {
|
|
146
|
-
const { attempt, compacted, model: ref, cutoff, handle, pollAt } = task.state.checkpoint;
|
|
185
|
+
const { attempt, compacted, model: ref, thinkingLevel, cutoff, handle, pollAt } = task.state.checkpoint;
|
|
186
|
+
if (cutOffTooOften(task, runtime))
|
|
187
|
+
return endCutOff(runtime, task, context);
|
|
188
|
+
// The host stopped while the poll's fetch was in flight: a failed attempt, as for a request. Checked before the
|
|
189
|
+
// time, which may have moved backwards on a replacement host; a sleeping poll is never cut off.
|
|
190
|
+
if (task.restarts !== undefined)
|
|
191
|
+
return interrupted(runtime, task.state.checkpoint, context);
|
|
192
|
+
if (pollAt > runtime.now())
|
|
193
|
+
return runtime.sleepUntil(pollAt, task.state.checkpoint, context);
|
|
147
194
|
const model = runtime.models.getModel(ref.provider, ref.modelId);
|
|
148
195
|
if (model === undefined)
|
|
149
196
|
return failNoModel(runtime, ref, context);
|
|
150
|
-
await runtime.
|
|
151
|
-
|
|
152
|
-
|
|
197
|
+
const message = await runtime.models.fetchDeferred(model, handle, {
|
|
198
|
+
signal: runtime.signal,
|
|
199
|
+
telemetry: context.telemetry,
|
|
200
|
+
});
|
|
201
|
+
const request = { attempt, compacted, model: ref, thinkingLevel, cutoff, pollAt };
|
|
153
202
|
await classify(runtime, request, message, context);
|
|
154
203
|
},
|
|
155
204
|
tools: async (task, runtime, context) => {
|
|
205
|
+
if (cutOffTooOften(task, runtime))
|
|
206
|
+
return endCutOff(runtime, task, context);
|
|
156
207
|
const { assistant, tools, pending } = task.state.checkpoint;
|
|
157
208
|
const [next, ...rest] = pending;
|
|
158
209
|
if (next === undefined)
|
|
@@ -160,8 +211,8 @@ export const GenerationTask = defineTask({
|
|
|
160
211
|
// Sequential round: start the next call and wait for it.
|
|
161
212
|
await runtime.commit(async (tx) => {
|
|
162
213
|
const live = await tx.doc(LiveDoc, runtime.conversationId);
|
|
163
|
-
const taskId = await createToolTask(tx, runtime, assistant, next);
|
|
164
214
|
const slot = live.tools?.find((slot) => slot.callId === next && slot.taskId === undefined);
|
|
215
|
+
const taskId = await createToolTask(tx, runtime, assistant, next, slot?.unoffered);
|
|
165
216
|
if (slot !== undefined)
|
|
166
217
|
slot.taskId = taskId;
|
|
167
218
|
const checkpoint = {
|
|
@@ -173,22 +224,50 @@ export const GenerationTask = defineTask({
|
|
|
173
224
|
return { status: "waiting", checkpoint, on: [taskId], policy: "allSettled" };
|
|
174
225
|
}, context);
|
|
175
226
|
},
|
|
227
|
+
/**
|
|
228
|
+
* Ask the `afterRun` hooks for tasks and create them, as the generation's children, in the commit that moves to
|
|
229
|
+
* `drain`; without any, end the run now.
|
|
230
|
+
*/
|
|
231
|
+
finish: async (task, runtime, context) => {
|
|
232
|
+
const { run } = task.state.checkpoint;
|
|
233
|
+
if (cutOffTooOften(task, runtime))
|
|
234
|
+
return endAnsweredCutOff(runtime, task, run.answer, context);
|
|
235
|
+
const prepared = [];
|
|
236
|
+
await runtime.hooks.each("afterRun", async (hook) => {
|
|
237
|
+
const asked = await hook(run, runtime, context);
|
|
238
|
+
if (asked !== undefined)
|
|
239
|
+
prepareRunEndTasks(runtime, asked.tasks, prepared);
|
|
240
|
+
});
|
|
241
|
+
if (prepared.length === 0)
|
|
242
|
+
return endAnsweredRun(runtime, run.answer, context);
|
|
243
|
+
await runtime.commit(async (tx) => {
|
|
244
|
+
const tasks = [];
|
|
245
|
+
for (const { task, input } of prepared) {
|
|
246
|
+
tasks.push(await tx.createTask(task, input, { ownership: { kind: "task", taskId: runtime.taskId } }));
|
|
247
|
+
}
|
|
248
|
+
const live = await tx.doc(LiveDoc, runtime.conversationId);
|
|
249
|
+
if (live.run?.taskId === runtime.taskId && live.run.afterRun !== undefined)
|
|
250
|
+
live.run.afterRun.tasks = tasks;
|
|
251
|
+
const checkpoint = { phase: "drain", answer: run.answer, tasks };
|
|
252
|
+
return { status: "waiting", checkpoint, on: tasks, policy: "allSettled" };
|
|
253
|
+
}, context);
|
|
254
|
+
},
|
|
255
|
+
/** The after-run tasks are terminal, whatever their outcomes: end the run. */
|
|
256
|
+
drain: async (task, runtime, context) => {
|
|
257
|
+
const { answer } = task.state.checkpoint;
|
|
258
|
+
if (cutOffTooOften(task, runtime))
|
|
259
|
+
return endAnsweredCutOff(runtime, task, answer, context);
|
|
260
|
+
return endAnsweredRun(runtime, answer, context);
|
|
261
|
+
},
|
|
176
262
|
},
|
|
177
263
|
abort: async (task, runtime, context) => {
|
|
178
264
|
const checkpoint = task.state.checkpoint;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
try {
|
|
183
|
-
await runtime.models.cancelDeferred(model, checkpoint.handle, { signal: runtime.signal });
|
|
184
|
-
}
|
|
185
|
-
catch (error) {
|
|
186
|
-
runtime.report(error);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
265
|
+
const polled = deferredOf(checkpoint);
|
|
266
|
+
if (polled !== undefined)
|
|
267
|
+
await cancelDeferred(runtime, polled, context);
|
|
190
268
|
const conversationId = runtime.conversationId;
|
|
191
|
-
// Runs after the round's tool tasks are terminal; calls never started get `aborted` results (spec §8.5)
|
|
269
|
+
// Runs after the round's tool tasks are terminal; calls never started get `aborted` results (spec §8.5), unoffered
|
|
270
|
+
// ones `tool_unavailable`.
|
|
192
271
|
const unstarted = checkpoint.phase === "tools"
|
|
193
272
|
? await readCalls(runtime, checkpoint.assistant, checkpoint.pending, context)
|
|
194
273
|
: [];
|
|
@@ -196,9 +275,14 @@ export const GenerationTask = defineTask({
|
|
|
196
275
|
const live = await tx.doc(LiveDoc, conversationId);
|
|
197
276
|
await convertPartial(tx, live, conversationId);
|
|
198
277
|
for (const call of unstarted) {
|
|
199
|
-
|
|
278
|
+
// A call its request did not offer could not have run either way.
|
|
279
|
+
const unoffered = live.tools?.find((slot) => slot.callId === call.id)?.unoffered === true;
|
|
280
|
+
const result = unoffered
|
|
281
|
+
? harnessError("tool_unavailable", `Tool ${call.name} is not available`)
|
|
282
|
+
: harnessError("aborted", `Tool ${call.name} was aborted`);
|
|
200
283
|
await appendToolResult(tx, conversationId, call, result, { timestamp: runtime.now() });
|
|
201
284
|
}
|
|
285
|
+
// In `finish` and `drain` the run answered already: it ends without a failure, its inputs stay `done`.
|
|
202
286
|
endRun(tx, live, runtime.taskId, { status: "unanswered", reason: "aborted" });
|
|
203
287
|
return { status: "terminal", outcome: { status: "aborted" } };
|
|
204
288
|
}, context);
|
|
@@ -210,9 +294,9 @@ async function readCalls(runtime, assistant, callIds, context) {
|
|
|
210
294
|
const calls = message?.role === "assistant" ? message.content.filter((content) => content.type === "toolCall") : [];
|
|
211
295
|
return callIds.flatMap((id) => calls.filter((call) => call.id === id).slice(0, 1));
|
|
212
296
|
}
|
|
213
|
-
/** A tool task for call `callId`, owned by the generation. */
|
|
214
|
-
function createToolTask(tx, runtime, assistant, callId) {
|
|
215
|
-
return tx.createTask(ToolTask, { kind: "model", assistant, callId }, { ownership: { kind: "task", taskId: runtime.taskId } });
|
|
297
|
+
/** A tool task for call `callId`, owned by the generation; `unoffered` for a call to a tool its request did not offer. */
|
|
298
|
+
function createToolTask(tx, runtime, assistant, callId, unoffered) {
|
|
299
|
+
return tx.createTask(ToolTask, { kind: "model", assistant, callId, ...(unoffered === undefined ? {} : { unoffered }) }, { ownership: { kind: "task", taskId: runtime.taskId } });
|
|
216
300
|
}
|
|
217
301
|
/**
|
|
218
302
|
* Which threshold compaction preparation starts before its request (spec §8.3): `blocking` above
|
|
@@ -245,23 +329,152 @@ function compactionFailure(outcome) {
|
|
|
245
329
|
/** Settle the run's inputs `unanswered` with `model_error` and fail with `text`. */
|
|
246
330
|
async function failModelError(runtime, text, context) {
|
|
247
331
|
await runtime.commit(async (tx) => {
|
|
332
|
+
const failure = await prepareFailure(tx, runtime);
|
|
248
333
|
const live = await tx.doc(LiveDoc, runtime.conversationId);
|
|
249
|
-
|
|
334
|
+
await endFailedRun(tx, runtime, live, failure, { status: "unanswered", reason: "model_error", detail: text });
|
|
250
335
|
return {
|
|
251
336
|
status: "terminal",
|
|
252
337
|
outcome: { status: "failed", error: { message: text, detail: { reason: "model_error" } } },
|
|
253
338
|
};
|
|
254
339
|
}, context);
|
|
255
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* The host stopped while this attempt's request or poll was in flight (`TaskRecord.restarts`): a failed attempt under
|
|
343
|
+
* `settings.retry` (spec §8.3). The committed partial becomes an aborted entry; the next attempt follows the backoff, a
|
|
344
|
+
* poll's by polling its deferred response again, or, with no retry left, the run ends `interrupted`.
|
|
345
|
+
*/
|
|
346
|
+
async function interrupted(runtime, checkpoint, context) {
|
|
347
|
+
const { attempt, compacted } = checkpoint;
|
|
348
|
+
const policy = runtime.settings.retry;
|
|
349
|
+
const retry = policy.enabled && attempt <= policy.maxRetries;
|
|
350
|
+
const until = retry ? runtime.now() + retryDelayMs(policy, attempt) : 0;
|
|
351
|
+
const polled = checkpoint.phase === "poll" ? checkpoint : undefined;
|
|
352
|
+
const text = polled === undefined
|
|
353
|
+
? `The host stopped while the model was answering, on attempt ${attempt}`
|
|
354
|
+
: `The host stopped while the deferred answer was fetched, on attempt ${attempt}`;
|
|
355
|
+
// A run that ends does not leave the provider's job running.
|
|
356
|
+
if (!retry && polled !== undefined)
|
|
357
|
+
await cancelDeferred(runtime, polled, context);
|
|
358
|
+
await runtime.commit(async (tx) => {
|
|
359
|
+
// Read before the first table write; only a failure uses it.
|
|
360
|
+
const failure = retry ? undefined : await prepareFailure(tx, runtime);
|
|
361
|
+
const live = await tx.doc(LiveDoc, runtime.conversationId);
|
|
362
|
+
await convertPartial(tx, live, runtime.conversationId);
|
|
363
|
+
if (failure === undefined) {
|
|
364
|
+
live.generation = { attempt, retry: { at: until, error: text } };
|
|
365
|
+
const poll = polled === undefined
|
|
366
|
+
? {}
|
|
367
|
+
: {
|
|
368
|
+
poll: {
|
|
369
|
+
model: polled.model,
|
|
370
|
+
...(polled.thinkingLevel === undefined ? {} : { thinkingLevel: polled.thinkingLevel }),
|
|
371
|
+
cutoff: polled.cutoff,
|
|
372
|
+
handle: polled.handle,
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
const next = { phase: "retry", attempt, ...(compacted === undefined ? {} : { compacted }), until, ...poll };
|
|
376
|
+
return { status: "sleeping", checkpoint: next, until };
|
|
377
|
+
}
|
|
378
|
+
await endFailedRun(tx, runtime, live, failure, { status: "unanswered", reason: "interrupted", detail: text });
|
|
379
|
+
return {
|
|
380
|
+
status: "terminal",
|
|
381
|
+
outcome: { status: "failed", error: { message: text, detail: { reason: "interrupted" } } },
|
|
382
|
+
};
|
|
383
|
+
}, context);
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* End a run whose generation was cut off `settings.restartLimit` times in a row (spec §5.4). At the limit it still
|
|
387
|
+
* keeps a committed partial as an aborted entry and cancels a deferred response it polled; past the limit that cleanup
|
|
388
|
+
* may be what cut it off, so it ends without it.
|
|
389
|
+
*/
|
|
390
|
+
async function endCutOff(runtime, task, context) {
|
|
391
|
+
const checkpoint = task.state.checkpoint;
|
|
392
|
+
const text = `The generation was interrupted ${cutOffCount(task)} in its ${checkpoint.phase} phase`;
|
|
393
|
+
const salvage = salvageable(task, runtime);
|
|
394
|
+
const polled = deferredOf(checkpoint);
|
|
395
|
+
if (salvage && polled !== undefined)
|
|
396
|
+
await cancelDeferred(runtime, polled, context);
|
|
397
|
+
await runtime.commit(async (tx) => {
|
|
398
|
+
const failure = await prepareFailure(tx, runtime);
|
|
399
|
+
const live = await tx.doc(LiveDoc, runtime.conversationId);
|
|
400
|
+
if (salvage)
|
|
401
|
+
await convertPartial(tx, live, runtime.conversationId);
|
|
402
|
+
await endFailedRun(tx, runtime, live, failure, { status: "unanswered", reason: "restart_limit", detail: text });
|
|
403
|
+
return {
|
|
404
|
+
status: "terminal",
|
|
405
|
+
outcome: { status: "failed", error: { message: text, detail: { reason: "restart_limit" } } },
|
|
406
|
+
};
|
|
407
|
+
}, context);
|
|
408
|
+
}
|
|
409
|
+
/** The deferred response a checkpoint polls, or will poll again after a retry's backoff. */
|
|
410
|
+
function deferredOf(checkpoint) {
|
|
411
|
+
if (checkpoint.phase === "poll")
|
|
412
|
+
return checkpoint;
|
|
413
|
+
return checkpoint.phase === "retry" ? checkpoint.poll : undefined;
|
|
414
|
+
}
|
|
415
|
+
/** Cancel a deferred response no attempt will poll again; a failure is reported, never thrown. */
|
|
416
|
+
async function cancelDeferred(runtime, checkpoint, context) {
|
|
417
|
+
const model = runtime.models.getModel(checkpoint.model.provider, checkpoint.model.modelId);
|
|
418
|
+
if (model === undefined)
|
|
419
|
+
return;
|
|
420
|
+
try {
|
|
421
|
+
await runtime.models.cancelDeferred(model, checkpoint.handle, {
|
|
422
|
+
signal: runtime.signal,
|
|
423
|
+
telemetry: context.telemetry,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
runtime.report(error, { kind: "host", callback: "models" });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
256
430
|
/** Settle the run's inputs `unanswered` with `no_model` and fail. */
|
|
257
431
|
async function failNoModel(runtime, ref, context) {
|
|
258
432
|
const message = ref === undefined ? "No model is configured" : `Model ${ref.provider}/${ref.modelId} is not available`;
|
|
259
433
|
await runtime.commit(async (tx) => {
|
|
434
|
+
const failure = await prepareFailure(tx, runtime);
|
|
260
435
|
const live = await tx.doc(LiveDoc, runtime.conversationId);
|
|
261
|
-
|
|
436
|
+
await endFailedRun(tx, runtime, live, failure, {
|
|
437
|
+
status: "unanswered",
|
|
438
|
+
reason: "no_model",
|
|
439
|
+
detail: message,
|
|
440
|
+
settlement: { status: "unanswered", reason: "no_model" },
|
|
441
|
+
});
|
|
262
442
|
return { status: "terminal", outcome: { status: "failed", error: { message, detail: { reason: "no_model" } } } };
|
|
263
443
|
}, context);
|
|
264
444
|
}
|
|
445
|
+
/** End the run without a request: its context has nothing the model would answer. */
|
|
446
|
+
async function failNothingToAnswer(runtime, context) {
|
|
447
|
+
const message = "The conversation has nothing to answer";
|
|
448
|
+
await runtime.commit(async (tx) => {
|
|
449
|
+
const failure = await prepareFailure(tx, runtime);
|
|
450
|
+
const live = await tx.doc(LiveDoc, runtime.conversationId);
|
|
451
|
+
await endFailedRun(tx, runtime, live, failure, { status: "unanswered", reason: "nothing_to_answer" });
|
|
452
|
+
const error = { message, detail: { reason: "nothing_to_answer" } };
|
|
453
|
+
return { status: "terminal", outcome: { status: "failed", error } };
|
|
454
|
+
}, context);
|
|
455
|
+
}
|
|
456
|
+
async function prepareFailure(tx, runtime) {
|
|
457
|
+
const settings = runtime.settings;
|
|
458
|
+
const policy = settings.afterFailedRun;
|
|
459
|
+
if (policy !== "continue")
|
|
460
|
+
return { policy };
|
|
461
|
+
return { policy, boundary: await prepareBoundary(tx, runtime.conversationId, settings) };
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* End a failed run, settling its inputs with `settlement`, and do with the inputs queued behind it what
|
|
465
|
+
* `settings.afterFailedRun` says (spec §6): keep them, start the next run with them, or withdraw them.
|
|
466
|
+
*/
|
|
467
|
+
async function endFailedRun(tx, runtime, live, failure, end) {
|
|
468
|
+
const conversationId = runtime.conversationId;
|
|
469
|
+
endRun(tx, live, runtime.taskId, end);
|
|
470
|
+
if (failure.policy === "withdraw")
|
|
471
|
+
await withdrawQueuedInputs(tx, conversationId, "run_failed");
|
|
472
|
+
if (failure.boundary === undefined)
|
|
473
|
+
return;
|
|
474
|
+
const { users, first } = await applyBoundary(tx, failure.boundary, "final", runtime.now());
|
|
475
|
+
if (users.length > 0)
|
|
476
|
+
await startRun(tx, conversationId, live, { inputs: users, first }, { keepFailure: true });
|
|
477
|
+
}
|
|
265
478
|
/**
|
|
266
479
|
* Append a committed partial left by an interrupted, aborted, faulted, or orphaned attempt as an aborted assistant
|
|
267
480
|
* entry; the caller replaces or removes `generation`.
|
|
@@ -273,10 +486,22 @@ export async function convertPartial(tx, live, conversationId) {
|
|
|
273
486
|
const message = copyJson(partial);
|
|
274
487
|
await appendAssistant(tx, conversationId, { ...message, stopReason: "aborted" });
|
|
275
488
|
}
|
|
489
|
+
/** The `beforeProviderRequest` replacement chain over one provider request body. */
|
|
490
|
+
async function providerPayload(runtime, model, payload, context) {
|
|
491
|
+
let current = payload;
|
|
492
|
+
await runtime.hooks.each("beforeProviderRequest", async (hook) => {
|
|
493
|
+
const replaced = await hook({ model, payload: current }, runtime, context);
|
|
494
|
+
if (replaced !== undefined)
|
|
495
|
+
current = replaced.payload;
|
|
496
|
+
});
|
|
497
|
+
return current;
|
|
498
|
+
}
|
|
276
499
|
/**
|
|
277
500
|
* Stream one request and return the terminal message. Partials commit as trailing writes at most every
|
|
278
|
-
* `progress.partialIntervalMs` (default 100 ms) with one commit in flight; `finally` stops
|
|
279
|
-
* commit, so no stale partial lands after the outcome.
|
|
501
|
+
* `progress.partialIntervalMs` (default 100 ms) with one commit in flight, deferred (`PROGRESS_COMMIT`); `finally` stops
|
|
502
|
+
* the throttle and awaits that commit, so no stale partial lands after the outcome. A commit appends what text and
|
|
503
|
+
* thinking grew by and applies the argument changes since the last one (`PartialChanges`), so its cost follows the new
|
|
504
|
+
* text, not the strings' length.
|
|
280
505
|
*/
|
|
281
506
|
async function streamResponse(runtime, model, messages, options, attempt, context) {
|
|
282
507
|
const interval = runtime.settings.progress.partialIntervalMs;
|
|
@@ -284,6 +509,7 @@ async function streamResponse(runtime, model, messages, options, attempt, contex
|
|
|
284
509
|
let timer;
|
|
285
510
|
let inFlight;
|
|
286
511
|
let stopped = false;
|
|
512
|
+
const changes = new PartialChanges();
|
|
287
513
|
const flush = () => {
|
|
288
514
|
timer = undefined;
|
|
289
515
|
const partial = pending;
|
|
@@ -291,14 +517,22 @@ async function streamResponse(runtime, model, messages, options, attempt, contex
|
|
|
291
517
|
if (partial === undefined || stopped)
|
|
292
518
|
return;
|
|
293
519
|
inFlight = (async () => {
|
|
294
|
-
// Copy synchronously: the provider keeps mutating its partial.
|
|
520
|
+
// Copy synchronously: the provider keeps mutating its partial. The changes since the last commit belong to it.
|
|
521
|
+
// `copyJson()` returns untyped JSON; a copy of a message is that message's JSON representation.
|
|
295
522
|
const message = copyJson(partial, { omitUndefinedProperties: true });
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
523
|
+
const taken = changes.take(message);
|
|
524
|
+
try {
|
|
525
|
+
await runtime.commit(async (tx) => {
|
|
526
|
+
const live = await tx.doc(LiveDoc, runtime.conversationId);
|
|
527
|
+
live.generation ??= { attempt };
|
|
528
|
+
changes.commit(live.generation, message, taken);
|
|
529
|
+
return undefined;
|
|
530
|
+
}, context, PROGRESS_COMMIT);
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
changes.restore(taken);
|
|
534
|
+
throw error;
|
|
535
|
+
}
|
|
302
536
|
})()
|
|
303
537
|
.catch((error) => {
|
|
304
538
|
// Rejections after an abort mark or close are expected; the committed state stays consistent.
|
|
@@ -308,7 +542,7 @@ async function streamResponse(runtime, model, messages, options, attempt, contex
|
|
|
308
542
|
.finally(() => {
|
|
309
543
|
inFlight = undefined;
|
|
310
544
|
if (pending !== undefined && !stopped)
|
|
311
|
-
timer =
|
|
545
|
+
timer = runtime.setTimer(flush, interval);
|
|
312
546
|
});
|
|
313
547
|
};
|
|
314
548
|
try {
|
|
@@ -318,15 +552,16 @@ async function streamResponse(runtime, model, messages, options, attempt, contex
|
|
|
318
552
|
// never gets past it, so it never leaves a partial.
|
|
319
553
|
if (event.type === "done" || event.type === "error" || event.partial.content.length === 0)
|
|
320
554
|
continue;
|
|
555
|
+
changes.observe(event);
|
|
321
556
|
pending = event.partial;
|
|
322
557
|
if (timer === undefined && inFlight === undefined)
|
|
323
|
-
timer =
|
|
558
|
+
timer = runtime.setTimer(flush, interval);
|
|
324
559
|
}
|
|
325
560
|
return await events.result();
|
|
326
561
|
}
|
|
327
562
|
finally {
|
|
328
563
|
stopped = true;
|
|
329
|
-
|
|
564
|
+
timer?.();
|
|
330
565
|
await inFlight;
|
|
331
566
|
}
|
|
332
567
|
}
|
|
@@ -335,7 +570,10 @@ async function classify(runtime, request, message, context) {
|
|
|
335
570
|
// An abort mark or close: the abort invocation or the reopened run handles the committed state.
|
|
336
571
|
runtime.signal.throwIfAborted();
|
|
337
572
|
const conversationId = runtime.conversationId;
|
|
338
|
-
const { attempt, compacted, model: ref, cutoff } = request;
|
|
573
|
+
const { attempt, compacted, model: ref, thinkingLevel, cutoff } = request;
|
|
574
|
+
// Record the requested level, as pi-agent-core's agent loop does.
|
|
575
|
+
if (thinkingLevel !== undefined)
|
|
576
|
+
message.thinkingLevel = thinkingLevel;
|
|
339
577
|
if (message.stopReason === "deferred" && message.deferred !== undefined) {
|
|
340
578
|
const handle = message.deferred;
|
|
341
579
|
const pollAt = Math.max(runtime.now() + (handle.pollAfterMs ?? DEFAULT_POLL_AFTER_MS), request.pollAt === undefined ? Number.NEGATIVE_INFINITY : request.pollAt + 1);
|
|
@@ -346,11 +584,12 @@ async function classify(runtime, request, message, context) {
|
|
|
346
584
|
attempt,
|
|
347
585
|
...(compacted === undefined ? {} : { compacted }),
|
|
348
586
|
model: ref,
|
|
587
|
+
...(thinkingLevel === undefined ? {} : { thinkingLevel }),
|
|
349
588
|
cutoff,
|
|
350
589
|
handle,
|
|
351
590
|
pollAt,
|
|
352
591
|
};
|
|
353
|
-
return { status: "
|
|
592
|
+
return { status: "sleeping", checkpoint, until: pollAt };
|
|
354
593
|
}, context);
|
|
355
594
|
return;
|
|
356
595
|
}
|
|
@@ -365,10 +604,10 @@ async function classify(runtime, request, message, context) {
|
|
|
365
604
|
// The retry and compaction policies govern the next attempt, so they are read now rather than pinned at preparation.
|
|
366
605
|
const settings = runtime.settings;
|
|
367
606
|
const overflow = message.stopReason === "error" && isContextOverflow(message);
|
|
368
|
-
|
|
369
|
-
|
|
607
|
+
const compaction = compactionPolicy(settings, ref);
|
|
608
|
+
if (overflow && compacted === undefined && compaction.enabled) {
|
|
370
609
|
const view = await runtime.context(conversationId, context, { at: cutoff });
|
|
371
|
-
if (selectCut(view,
|
|
610
|
+
if (selectCut(view, compaction.keepRecentTokens) !== undefined) {
|
|
372
611
|
const text = message.errorMessage ?? "Context overflow";
|
|
373
612
|
await runtime.commit(async (tx) => {
|
|
374
613
|
const live = await tx.doc(LiveDoc, conversationId);
|
|
@@ -390,9 +629,11 @@ async function classify(runtime, request, message, context) {
|
|
|
390
629
|
attempt <= policy.maxRetries;
|
|
391
630
|
const until = retry ? runtime.now() + retryDelayMs(policy, attempt) : 0;
|
|
392
631
|
await runtime.commit(async (tx) => {
|
|
632
|
+
// Read before the first table write; only a failure uses it.
|
|
633
|
+
const failure = retry ? undefined : await prepareFailure(tx, runtime);
|
|
393
634
|
const live = await tx.doc(LiveDoc, conversationId);
|
|
394
635
|
await appendAssistant(tx, conversationId, message);
|
|
395
|
-
if (
|
|
636
|
+
if (failure === undefined) {
|
|
396
637
|
live.generation = { attempt, retry: { at: until, error: message.errorMessage ?? "" } };
|
|
397
638
|
const checkpoint = {
|
|
398
639
|
phase: "retry",
|
|
@@ -400,10 +641,10 @@ async function classify(runtime, request, message, context) {
|
|
|
400
641
|
...(compacted === undefined ? {} : { compacted }),
|
|
401
642
|
until,
|
|
402
643
|
};
|
|
403
|
-
return { status: "
|
|
644
|
+
return { status: "sleeping", checkpoint, until };
|
|
404
645
|
}
|
|
405
646
|
const text = message.errorMessage ?? `Model response ended with stop reason ${message.stopReason}`;
|
|
406
|
-
|
|
647
|
+
await endFailedRun(tx, runtime, live, failure, { status: "unanswered", reason: "model_error", detail: text });
|
|
407
648
|
return {
|
|
408
649
|
status: "terminal",
|
|
409
650
|
outcome: { status: "failed", error: { message: text, detail: { reason: "model_error" } } },
|
|
@@ -412,8 +653,9 @@ async function classify(runtime, request, message, context) {
|
|
|
412
653
|
}
|
|
413
654
|
/**
|
|
414
655
|
* A final answer; the final boundary places queued items (spec §6). The first `onYield` continuation appends a user
|
|
415
|
-
* message and hands the run to a successor generation, but only when the
|
|
416
|
-
* Otherwise the run's inputs settle `done
|
|
656
|
+
* message and hands the run to a successor generation, but only when the inbox holds no user item and no reset.
|
|
657
|
+
* Otherwise the run's inputs settle `done`; with `afterRun` hooks selected the run waits for the tasks they ask for,
|
|
658
|
+
* in `finish` and `drain`, before the final boundary; without, selected user items start the next run now.
|
|
417
659
|
*/
|
|
418
660
|
async function answer(runtime, message, context) {
|
|
419
661
|
let continuation;
|
|
@@ -422,6 +664,7 @@ async function answer(runtime, message, context) {
|
|
|
422
664
|
return;
|
|
423
665
|
continuation = (await hook(message, runtime, context))?.continue;
|
|
424
666
|
});
|
|
667
|
+
const afterRun = await asksAfterRun(runtime, context);
|
|
425
668
|
const conversationId = runtime.conversationId;
|
|
426
669
|
await runtime.commit(async (tx) => {
|
|
427
670
|
// Queue modes are read on the Session line, when the boundary is decided.
|
|
@@ -429,24 +672,135 @@ async function answer(runtime, message, context) {
|
|
|
429
672
|
const live = await tx.doc(LiveDoc, conversationId);
|
|
430
673
|
const entry = await appendAssistant(tx, conversationId, message);
|
|
431
674
|
const result = { status: "terminal", outcome: { status: "completed", result: { entryId: entry.id } } };
|
|
432
|
-
const
|
|
433
|
-
if (
|
|
434
|
-
const
|
|
675
|
+
const continues = takesContinuation(continuation, boundary) ? continuation : undefined;
|
|
676
|
+
if (continues === undefined && afterRun) {
|
|
677
|
+
const held = holdRun(tx, live, runtime, { reason: "answer", answer: entry.id, last: entry.id });
|
|
678
|
+
if (held !== undefined)
|
|
679
|
+
return held;
|
|
680
|
+
}
|
|
681
|
+
const { users, first } = await applyBoundary(tx, boundary, "final", runtime.now());
|
|
682
|
+
if (continues !== undefined) {
|
|
683
|
+
const user = { role: "user", content: continues, timestamp: runtime.now() };
|
|
435
684
|
await tx.appendEntry(UserEntry, conversationId, { model: [user] });
|
|
685
|
+
// The continuation starts the run's turn count over: bounding `onYield` loops is the extension's job.
|
|
686
|
+
if (live.run?.taskId === runtime.taskId)
|
|
687
|
+
live.run.turns = 0;
|
|
436
688
|
handOver(live, runtime.taskId, await createGeneration(tx, conversationId));
|
|
437
689
|
delete live.generation;
|
|
438
690
|
return result;
|
|
439
691
|
}
|
|
440
692
|
endRun(tx, live, runtime.taskId, { status: "done", answer: entry.id });
|
|
441
693
|
if (users.length > 0)
|
|
442
|
-
await startRun(tx, conversationId, live, users);
|
|
694
|
+
await startRun(tx, conversationId, live, { inputs: users, first });
|
|
443
695
|
return result;
|
|
444
696
|
}, context);
|
|
445
697
|
}
|
|
698
|
+
/** Whether the phase's agent selects an `afterRun` hook, so a run that answers waits for its tasks (spec §8.3). */
|
|
699
|
+
async function asksAfterRun(runtime, context) {
|
|
700
|
+
const hooks = agentHooks(await runtime.agent(context), GenerationTask.definition.name);
|
|
701
|
+
return hooks.some(({ handlers }) => typeof handlers.afterRun === "function");
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Commit A of a run that answered with `afterRun` hooks selected (spec §8.3): settle the run's inputs `done` with the
|
|
705
|
+
* answer and keep `pi.live.run` for the after-run tasks, then ask the hooks in `finish`. The final boundary waits for
|
|
706
|
+
* the run's end, so queued items stay queued.
|
|
707
|
+
*/
|
|
708
|
+
function holdRun(tx, live, runtime, end) {
|
|
709
|
+
const run = live.run;
|
|
710
|
+
if (run?.taskId !== runtime.taskId)
|
|
711
|
+
return undefined;
|
|
712
|
+
for (const id of run.inputs)
|
|
713
|
+
tx.settleSubmission(id, { status: "done", answer: end.answer });
|
|
714
|
+
run.inputs = [];
|
|
715
|
+
run.afterRun = {};
|
|
716
|
+
delete live.generation;
|
|
717
|
+
delete live.tools;
|
|
718
|
+
delete live.nestedTools;
|
|
719
|
+
// A run started before `first` was recorded has its answer as its first entry.
|
|
720
|
+
const summary = { ...end, first: run.first ?? end.answer };
|
|
721
|
+
return { status: "running", checkpoint: { phase: "finish", run: summary } };
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Check the tasks one `afterRun` hook asked for, off the line, and add those that pass to `prepared`, so a broken request
|
|
725
|
+
* is skipped instead of failing the creating commit: its kind must be an installed task that is not built-in, its input
|
|
726
|
+
* JSON, and its definition's `initial()` must return a JSON checkpoint of one of its phases. The task is created from
|
|
727
|
+
* the installed definition, as every task runs under it, with that checkpoint, so `initial()` runs once. Then throws
|
|
728
|
+
* the rejections, which `hooks.each` reports against the hook's extension.
|
|
729
|
+
*/
|
|
730
|
+
function prepareRunEndTasks(runtime, requests, prepared) {
|
|
731
|
+
const rejected = [];
|
|
732
|
+
for (const request of requests) {
|
|
733
|
+
try {
|
|
734
|
+
const name = request.task.definition.name;
|
|
735
|
+
if (name.startsWith("pi."))
|
|
736
|
+
throw new Error(`An afterRun hook cannot start the built-in task ${name}`);
|
|
737
|
+
const installed = runtime.registry.task(name);
|
|
738
|
+
if (installed === undefined)
|
|
739
|
+
throw new Error(`Task ${name} is not installed`);
|
|
740
|
+
const input = copyJson(request.input, { omitUndefinedProperties: true });
|
|
741
|
+
// The registry erases definitions; an installed one is a task definition of its kind.
|
|
742
|
+
const definition = installed.definition;
|
|
743
|
+
const checkpoint = copyJson(definition.initial(input), { omitUndefinedProperties: true });
|
|
744
|
+
if (!Object.hasOwn(definition.phases, checkpoint.phase)) {
|
|
745
|
+
throw new Error(`Task ${name} starts at the unknown phase ${String(checkpoint.phase)}`);
|
|
746
|
+
}
|
|
747
|
+
prepared.push({ task: { definition: { ...definition, initial: () => checkpoint } }, input });
|
|
748
|
+
}
|
|
749
|
+
catch (error) {
|
|
750
|
+
rejected.push(error);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (rejected.length === 1)
|
|
754
|
+
throw rejected[0];
|
|
755
|
+
if (rejected.length > 1)
|
|
756
|
+
throw new AggregateError(rejected, `${rejected.length} afterRun task requests were rejected`);
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Commit B of a run that answered (spec §8.3): its after-run tasks are terminal, or there were none. Place what the
|
|
760
|
+
* final boundary selects, end the run, and start the next one for selected user items.
|
|
761
|
+
*/
|
|
762
|
+
async function endAnsweredRun(runtime, answer, context) {
|
|
763
|
+
const conversationId = runtime.conversationId;
|
|
764
|
+
await runtime.commit(async (tx) => {
|
|
765
|
+
const boundary = await prepareBoundary(tx, conversationId, runtime.settings);
|
|
766
|
+
const live = await tx.doc(LiveDoc, conversationId);
|
|
767
|
+
const { users, first } = await applyBoundary(tx, boundary, "final", runtime.now());
|
|
768
|
+
endRun(tx, live, runtime.taskId, { status: "done", answer });
|
|
769
|
+
if (users.length > 0)
|
|
770
|
+
await startRun(tx, conversationId, live, { inputs: users, first });
|
|
771
|
+
return { status: "terminal", outcome: { status: "completed", result: { entryId: answer } } };
|
|
772
|
+
}, context);
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* End a run that answered once its `finish` or `drain` phase was cut off `settings.restartLimit` times (spec §5.4):
|
|
776
|
+
* the run answered, so it ends `completed`, without a failure and without the tasks it would still have waited for,
|
|
777
|
+
* marked so the scheduler reports it `task_interrupted`. Queued items stay queued, as after a failed run.
|
|
778
|
+
*/
|
|
779
|
+
async function endAnsweredCutOff(runtime, task, answer, context) {
|
|
780
|
+
const text = `The generation was interrupted ${cutOffCount(task)} in its ${task.state.checkpoint.phase} phase`;
|
|
781
|
+
await runtime.commit(async (tx) => {
|
|
782
|
+
const live = await tx.doc(LiveDoc, runtime.conversationId);
|
|
783
|
+
endRun(tx, live, runtime.taskId, { status: "done", answer });
|
|
784
|
+
const completed = { status: "terminal", outcome: { status: "completed", result: { entryId: answer } } };
|
|
785
|
+
return endedByCrashes(completed, text);
|
|
786
|
+
}, context);
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Whether an answer's `onYield` continuation is taken (spec §8.3), decided from the inbox before the final boundary
|
|
790
|
+
* places anything: only when it holds no user item, of which the boundary would select one, and no reset, which would
|
|
791
|
+
* otherwise run first. A continuation starts the turn count over.
|
|
792
|
+
*/
|
|
793
|
+
function takesContinuation(continuation, boundary) {
|
|
794
|
+
const items = boundary.inbox.items;
|
|
795
|
+
return (continuation !== undefined &&
|
|
796
|
+
!items.some((item) => item.mode === "steer" || item.mode === "followUp") &&
|
|
797
|
+
!hasQueuedReset(items));
|
|
798
|
+
}
|
|
446
799
|
/**
|
|
447
|
-
* Append the tool-calling answer and start its tool round in one commit (spec §8.3).
|
|
448
|
-
*
|
|
449
|
-
*
|
|
800
|
+
* Append the tool-calling answer and start its tool round in one commit (spec §8.3). Every call gets a tool task owned by
|
|
801
|
+
* the generation, in call order, only the first one now when the round is sequential; a call to a tool the request did
|
|
802
|
+
* not offer is flagged, and its task settles `tool_unavailable`. The generation then waits for them in its `tools`
|
|
803
|
+
* phase, keeping the run.
|
|
450
804
|
*/
|
|
451
805
|
async function startToolRound(runtime, request, message, calls, context) {
|
|
452
806
|
const conversationId = runtime.conversationId;
|
|
@@ -463,30 +817,33 @@ async function startToolRound(runtime, request, message, calls, context) {
|
|
|
463
817
|
const tools = [];
|
|
464
818
|
const pending = [];
|
|
465
819
|
for (const call of calls) {
|
|
466
|
-
|
|
467
|
-
const unavailable = harnessError("tool_unavailable", `Tool ${call.name} is not available`);
|
|
468
|
-
const result = await appendToolResult(tx, conversationId, call, unavailable, { timestamp: runtime.now() });
|
|
469
|
-
slots.push({ callId: call.id, name: call.name, status: "done", entry: result.id });
|
|
470
|
-
continue;
|
|
471
|
-
}
|
|
820
|
+
const unoffered = offered.has(call.name) ? {} : { unoffered: true };
|
|
472
821
|
if (sequential && tools.length > 0) {
|
|
473
822
|
pending.push(call.id);
|
|
474
|
-
slots.push({ callId: call.id, name: call.name, status: "pending" });
|
|
823
|
+
slots.push({ callId: call.id, name: call.name, status: "pending", ...unoffered });
|
|
475
824
|
continue;
|
|
476
825
|
}
|
|
477
|
-
const taskId = await createToolTask(tx, runtime, entry.id, call.id);
|
|
826
|
+
const taskId = await createToolTask(tx, runtime, entry.id, call.id, unoffered.unoffered);
|
|
478
827
|
tools.push(taskId);
|
|
479
|
-
slots.push({ callId: call.id, name: call.name, taskId, status: "pending" });
|
|
828
|
+
slots.push({ callId: call.id, name: call.name, taskId, status: "pending", ...unoffered });
|
|
480
829
|
}
|
|
481
830
|
delete live.generation;
|
|
482
831
|
live.tools = slots;
|
|
832
|
+
if (live.run?.taskId === runtime.taskId) {
|
|
833
|
+
// A tool round is one turn of the run (`AgentState.maxTurns`).
|
|
834
|
+
live.run.turns += 1;
|
|
835
|
+
// A run its boundary started without placing anything, as a continue after an answer, begins here.
|
|
836
|
+
live.run.first ??= entry.id;
|
|
837
|
+
}
|
|
483
838
|
const checkpoint = { phase: "tools", assistant: entry.id, tools, pending };
|
|
484
839
|
return { status: "waiting", checkpoint, on: tools, policy: "allSettled" };
|
|
485
840
|
}, context);
|
|
486
841
|
}
|
|
487
842
|
/**
|
|
488
|
-
* The round's tools are terminal: apply their controls and
|
|
489
|
-
* `
|
|
843
|
+
* The round's tools are terminal: apply their controls, append a `handoff`'s reset, and then end the run at the final
|
|
844
|
+
* boundary (`terminate`), end it like a failure at the conversation's turn limit (`turn_limit`), end it at a queued
|
|
845
|
+
* reset, or hand it to the next generation at the `postTools` boundary, in the handoff's new context when there is one
|
|
846
|
+
* (spec §8.5).
|
|
490
847
|
*/
|
|
491
848
|
async function finishToolRound(runtime, assistant, tools, context) {
|
|
492
849
|
const conversationId = runtime.conversationId;
|
|
@@ -500,36 +857,60 @@ async function finishToolRound(runtime, assistant, tools, context) {
|
|
|
500
857
|
const slots = (await runtime.snapshot(LiveDoc, conversationId, context))?.tools ?? [];
|
|
501
858
|
const results = slots.flatMap((slot) => (slot.entry === undefined ? [] : [slot.entry]));
|
|
502
859
|
await runtime.hooks.each("afterTools", (hook) => hook(assistant, results, runtime, context));
|
|
503
|
-
//
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
const
|
|
860
|
+
// In call order, the slots' order; a slot without a task (faulted or orphaned before it got one) has no control.
|
|
861
|
+
const ordered = slots.map((slot) => (slot.taskId === undefined ? undefined : controls.get(slot.taskId)));
|
|
862
|
+
// Every call of the round must ask to terminate.
|
|
863
|
+
const terminate = slots.length > 0 && ordered.every((control) => control?.terminate === true);
|
|
864
|
+
const added = ordered.flatMap((control) => control?.addTools ?? []);
|
|
507
865
|
// The last handoff in call order wins.
|
|
508
|
-
const handoff =
|
|
866
|
+
const handoff = ordered.findLast((control) => control?.handoff !== undefined)?.handoff;
|
|
867
|
+
// Read from the phase's agent, as every agent field; a `configure()` during the round applies at its end.
|
|
868
|
+
const maxTurns = (await runtime.agent(context)).maxTurns;
|
|
869
|
+
const afterRun = terminate && (await asksAfterRun(runtime, context));
|
|
509
870
|
await runtime.commit(async (tx) => {
|
|
510
871
|
const boundary = await prepareBoundary(tx, conversationId, runtime.settings);
|
|
511
872
|
if (added.length > 0)
|
|
512
873
|
await addTools(tx, conversationId, added);
|
|
513
874
|
const live = await tx.doc(LiveDoc, conversationId);
|
|
514
875
|
const now = runtime.now();
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
}
|
|
521
|
-
|
|
876
|
+
// The run's last entry so far: the newest result, or the assistant when no result has an entry.
|
|
877
|
+
let last = results.reduce((newest, id) => (id > newest ? id : newest), assistant);
|
|
878
|
+
if (handoff !== undefined) {
|
|
879
|
+
// The handoff's reset precedes every queued item, so queued writes and selected steers land in its context.
|
|
880
|
+
const message = { role: "user", content: handoff, timestamp: now };
|
|
881
|
+
const entry = await tx.appendEntry(ResetEntry, conversationId, { head: "self", model: [message] });
|
|
882
|
+
boundary.head = entry.id;
|
|
883
|
+
last = entry.id;
|
|
884
|
+
}
|
|
885
|
+
const turns = live.run?.taskId === runtime.taskId ? live.run.turns : 0;
|
|
886
|
+
const held = afterRun ? holdRun(tx, live, runtime, { reason: "terminate", answer: assistant, last }) : undefined;
|
|
887
|
+
if (held !== undefined)
|
|
888
|
+
return held;
|
|
889
|
+
if (terminate) {
|
|
890
|
+
const { users, first } = await applyBoundary(tx, boundary, "final", now);
|
|
522
891
|
endRun(tx, live, runtime.taskId, { status: "done", answer: assistant });
|
|
523
892
|
if (users.length > 0)
|
|
524
|
-
await startRun(tx, conversationId, live, users);
|
|
893
|
+
await startRun(tx, conversationId, live, { inputs: users, first });
|
|
894
|
+
}
|
|
895
|
+
else if (maxTurns !== undefined && turns >= maxTurns) {
|
|
896
|
+
// The turn limit ends the run like a failure: `afterFailedRun` decides the queued items, and a continue resumes
|
|
897
|
+
// it on the round's results. The table was read for the boundary already, before any write.
|
|
898
|
+
const policy = runtime.settings.afterFailedRun;
|
|
899
|
+
const failure = { policy, ...(policy === "continue" ? { boundary } : {}) };
|
|
900
|
+
await endFailedRun(tx, runtime, live, failure, {
|
|
901
|
+
status: "unanswered",
|
|
902
|
+
reason: "turn_limit",
|
|
903
|
+
detail: turns === 1 ? "1 turn" : `${turns} turns`,
|
|
904
|
+
});
|
|
525
905
|
}
|
|
526
906
|
else {
|
|
527
|
-
const { users, reset } = await applyBoundary(tx, boundary, "postTools", now);
|
|
907
|
+
const { users, first, reset } = await applyBoundary(tx, boundary, "postTools", now);
|
|
528
908
|
if (reset) {
|
|
529
909
|
// The queued reset cut the run's context before an answer.
|
|
530
910
|
endRun(tx, live, runtime.taskId, { status: "unanswered", reason: "reset" });
|
|
531
|
-
if (users.length > 0)
|
|
532
|
-
await startRun(tx, conversationId, live, users);
|
|
911
|
+
if (users.length > 0) {
|
|
912
|
+
await startRun(tx, conversationId, live, { inputs: users, first }, { keepFailure: true });
|
|
913
|
+
}
|
|
533
914
|
}
|
|
534
915
|
else {
|
|
535
916
|
delete live.tools;
|
|
@@ -550,13 +931,21 @@ async function appendAssistant(tx, conversationId, message) {
|
|
|
550
931
|
await recordUsage(tx, conversationId, "models", `${message.provider}/${message.model}`, message.usage);
|
|
551
932
|
return tx.appendEntry(AssistantEntry, conversationId, { model: [message] });
|
|
552
933
|
}
|
|
553
|
-
/**
|
|
554
|
-
|
|
555
|
-
|
|
934
|
+
/**
|
|
935
|
+
* Start a run for `inputs`, placed input submissions, whose first entry is `first` (`LiveRun.first`): a new generation
|
|
936
|
+
* takes `pi.live.run`. Removes `runFailure`, unless `keepFailure`: a run started in the commit that ended the failed one
|
|
937
|
+
* keeps its failure.
|
|
938
|
+
*/
|
|
939
|
+
export async function startRun(tx, conversationId, live, run, options) {
|
|
940
|
+
const { inputs, first } = run;
|
|
941
|
+
const taskId = await createGeneration(tx, conversationId, { submissionIds: inputs });
|
|
942
|
+
live.run = { taskId, inputs, turns: 0, ...(first === undefined ? {} : { first }) };
|
|
943
|
+
if (options?.keepFailure !== true)
|
|
944
|
+
delete live.runFailure;
|
|
556
945
|
}
|
|
557
946
|
/** A generation owned by its conversation. */
|
|
558
|
-
function createGeneration(tx, conversationId) {
|
|
559
|
-
return tx.createTask(GenerationTask, {}, { ownership: { kind: "conversation" }, conversationId });
|
|
947
|
+
function createGeneration(tx, conversationId, telemetry = {}) {
|
|
948
|
+
return tx.createTask(GenerationTask, {}, { ownership: { kind: "conversation" }, conversationId, ...telemetry });
|
|
560
949
|
}
|
|
561
950
|
/** Hand run control from `from` to `to`; the run's inputs move with it. */
|
|
562
951
|
export function handOver(live, from, to) {
|