@earendil-works/pi-durable 0.99.1 → 0.99.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +435 -46
- package/dist/entries.d.ts +20 -2
- package/dist/entries.d.ts.map +1 -1
- package/dist/entries.js +14 -1
- package/dist/entries.js.map +1 -1
- package/dist/env/index.d.ts +13 -38
- package/dist/env/index.d.ts.map +1 -1
- package/dist/env/index.js +2 -0
- package/dist/env/index.js.map +1 -1
- package/dist/env/node.d.ts.map +1 -1
- package/dist/env/node.js +51 -58
- package/dist/env/node.js.map +1 -1
- package/dist/harness/compaction.d.ts +56 -0
- package/dist/harness/compaction.d.ts.map +1 -0
- package/dist/harness/compaction.js +385 -0
- package/dist/harness/compaction.js.map +1 -0
- package/dist/harness/config.d.ts +10 -1
- package/dist/harness/config.d.ts.map +1 -1
- package/dist/harness/config.js +6 -0
- package/dist/harness/config.js.map +1 -1
- package/dist/harness/context.d.ts +8 -0
- package/dist/harness/context.d.ts.map +1 -1
- package/dist/harness/context.js +32 -21
- package/dist/harness/context.js.map +1 -1
- package/dist/harness/events.d.ts +166 -0
- package/dist/harness/events.d.ts.map +1 -0
- package/dist/harness/events.js +319 -0
- package/dist/harness/events.js.map +1 -0
- package/dist/harness/generation.d.ts +37 -6
- package/dist/harness/generation.d.ts.map +1 -1
- package/dist/harness/generation.js +350 -72
- package/dist/harness/generation.js.map +1 -1
- package/dist/harness/harness.d.ts.map +1 -1
- package/dist/harness/harness.js +124 -3
- package/dist/harness/harness.js.map +1 -1
- package/dist/harness/inbox.d.ts +57 -0
- package/dist/harness/inbox.d.ts.map +1 -0
- package/dist/harness/inbox.js +99 -0
- package/dist/harness/inbox.js.map +1 -0
- package/dist/harness/json.d.ts +10 -0
- package/dist/harness/json.d.ts.map +1 -0
- package/dist/harness/json.js +33 -0
- package/dist/harness/json.js.map +1 -0
- package/dist/harness/live.d.ts +66 -7
- package/dist/harness/live.d.ts.map +1 -1
- package/dist/harness/live.js +71 -7
- package/dist/harness/live.js.map +1 -1
- package/dist/harness/output.d.ts +62 -0
- package/dist/harness/output.d.ts.map +1 -0
- package/dist/harness/output.js +261 -0
- package/dist/harness/output.js.map +1 -0
- package/dist/harness/prompt.d.ts +10 -5
- package/dist/harness/prompt.d.ts.map +1 -1
- package/dist/harness/prompt.js +66 -13
- package/dist/harness/prompt.js.map +1 -1
- package/dist/harness/registry.d.ts.map +1 -1
- package/dist/harness/registry.js +11 -2
- package/dist/harness/registry.js.map +1 -1
- package/dist/harness/scheduler.d.ts +37 -12
- package/dist/harness/scheduler.d.ts.map +1 -1
- package/dist/harness/scheduler.js +632 -72
- package/dist/harness/scheduler.js.map +1 -1
- package/dist/harness/submissions.d.ts +12 -7
- package/dist/harness/submissions.d.ts.map +1 -1
- package/dist/harness/submissions.js +72 -33
- package/dist/harness/submissions.js.map +1 -1
- package/dist/harness/tool.d.ts +36 -0
- package/dist/harness/tool.d.ts.map +1 -0
- package/dist/harness/tool.js +412 -0
- package/dist/harness/tool.js.map +1 -0
- package/dist/harness/types.d.ts +185 -11
- package/dist/harness/types.d.ts.map +1 -1
- package/dist/harness/types.js.map +1 -1
- package/dist/harness/usage.d.ts +18 -0
- package/dist/harness/usage.d.ts.map +1 -0
- package/dist/harness/usage.js +57 -0
- package/dist/harness/usage.js.map +1 -0
- package/dist/harness/view.d.ts +43 -0
- package/dist/harness/view.d.ts.map +1 -0
- package/dist/harness/view.js +194 -0
- package/dist/harness/view.js.map +1 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/session/observation.d.ts +18 -11
- package/dist/session/observation.d.ts.map +1 -1
- package/dist/session/observation.js +24 -13
- package/dist/session/observation.js.map +1 -1
- package/dist/session/session.d.ts +10 -1
- package/dist/session/session.d.ts.map +1 -1
- package/dist/session/session.js +18 -4
- package/dist/session/session.js.map +1 -1
- package/dist/session/transaction.d.ts +10 -1
- package/dist/session/transaction.d.ts.map +1 -1
- package/dist/session/transaction.js +83 -27
- package/dist/session/transaction.js.map +1 -1
- package/dist/storage/jsonl/storage.d.ts +22 -2
- package/dist/storage/jsonl/storage.d.ts.map +1 -1
- package/dist/storage/jsonl/storage.js +1 -1
- package/dist/storage/jsonl/storage.js.map +1 -1
- package/dist/storage/memory.js +1 -1
- package/dist/storage/memory.js.map +1 -1
- package/dist/storage/sqlite/migrations.js +1 -1
- package/dist/storage/sqlite/migrations.js.map +1 -1
- package/dist/testing/storage-benchmark.d.ts.map +1 -1
- package/dist/testing/storage-benchmark.js +0 -1
- 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 +34 -2
- package/dist/testing/storage-conformance.js.map +1 -1
- package/dist/tools/bash.d.ts +28 -0
- package/dist/tools/bash.d.ts.map +1 -0
- package/dist/tools/bash.js +69 -0
- package/dist/tools/bash.js.map +1 -0
- package/dist/tools/edit-diff.d.ts +89 -0
- package/dist/tools/edit-diff.d.ts.map +1 -0
- package/dist/tools/edit-diff.js +386 -0
- package/dist/tools/edit-diff.js.map +1 -0
- package/dist/tools/edit.d.ts +18 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +116 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/env.d.ts +5 -0
- package/dist/tools/env.d.ts.map +1 -0
- package/dist/tools/env.js +7 -0
- package/dist/tools/env.js.map +1 -0
- package/dist/tools/file-mutation-queue.d.ts +5 -0
- package/dist/tools/file-mutation-queue.d.ts.map +1 -0
- package/dist/tools/file-mutation-queue.js +46 -0
- package/dist/tools/file-mutation-queue.js.map +1 -0
- package/dist/tools/image.d.ts +2 -0
- package/dist/tools/image.d.ts.map +1 -0
- package/dist/tools/image.js +92 -0
- package/dist/tools/image.js.map +1 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +5 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/path-utils.d.ts +5 -0
- package/dist/tools/path-utils.d.ts.map +1 -0
- package/dist/tools/path-utils.js +26 -0
- package/dist/tools/path-utils.js.map +1 -0
- package/dist/tools/read.d.ts +17 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +100 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/write.d.ts +10 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +30 -0
- package/dist/tools/write.js.map +1 -0
- package/dist/{env/utils/truncate.d.ts → truncate.d.ts} +1 -17
- package/dist/truncate.d.ts.map +1 -0
- package/dist/truncate.js +146 -0
- package/dist/truncate.js.map +1 -0
- package/dist/types.d.ts +86 -19
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/docs/pico-v5-handoff.md +366 -86
- package/docs/pico-v5.md +1373 -283
- package/package.json +11 -3
- package/dist/env/utils/adaptive-publisher.d.ts +0 -24
- package/dist/env/utils/adaptive-publisher.d.ts.map +0 -1
- package/dist/env/utils/adaptive-publisher.js +0 -79
- package/dist/env/utils/adaptive-publisher.js.map +0 -1
- package/dist/env/utils/output-capture.d.ts +0 -31
- package/dist/env/utils/output-capture.d.ts.map +0 -1
- package/dist/env/utils/output-capture.js +0 -238
- package/dist/env/utils/output-capture.js.map +0 -1
- package/dist/env/utils/truncate.d.ts.map +0 -1
- package/dist/env/utils/truncate.js +0 -294
- package/dist/env/utils/truncate.js.map +0 -1
|
@@ -1,36 +1,67 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import type {
|
|
3
|
-
import type {
|
|
1
|
+
import { type Draft } from "@earendil-works/chord";
|
|
2
|
+
import type { DeferredHandle, ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { ConversationId, EntryId, SubmissionId, TaskId, Tx } from "../types.ts";
|
|
4
|
+
import { type LiveState } from "./live.ts";
|
|
5
|
+
import { type ToolTaskResult } from "./tool.ts";
|
|
6
|
+
import type { CompactionResult, ConversationStreamOptions, GenerationHooks, ModelRef, ToolExecutionMode } from "./types.ts";
|
|
4
7
|
export type GenerationInput = Record<string, never>;
|
|
5
8
|
export type GenerationCheckpoint = {
|
|
6
9
|
phase: "prepare";
|
|
7
10
|
attempt: number;
|
|
11
|
+
/** The blocking compaction this generation waited for; it starts no other compaction (spec §8.3). */
|
|
12
|
+
compacted?: TaskId<CompactionResult>;
|
|
13
|
+
/** Error text of the overflow that started `compacted`; checked once when `prepare` resumes. */
|
|
14
|
+
overflow?: string;
|
|
8
15
|
} | {
|
|
9
16
|
phase: "request";
|
|
10
17
|
attempt: number;
|
|
18
|
+
compacted?: TaskId<CompactionResult>;
|
|
11
19
|
model: ModelRef;
|
|
12
20
|
thinkingLevel: ModelThinkingLevel;
|
|
13
21
|
/** Configured request options when preparation committed; a resend after recovery uses them unchanged. */
|
|
14
22
|
streamOptions: ConversationStreamOptions;
|
|
23
|
+
toolExecution: ToolExecutionMode;
|
|
15
24
|
/** Newest entry included in the request. */
|
|
16
25
|
cutoff: EntryId;
|
|
17
26
|
} | {
|
|
18
27
|
phase: "retry";
|
|
19
28
|
attempt: number;
|
|
29
|
+
compacted?: TaskId<CompactionResult>;
|
|
20
30
|
until: number;
|
|
21
31
|
} | {
|
|
22
32
|
phase: "poll";
|
|
23
33
|
attempt: number;
|
|
34
|
+
compacted?: TaskId<CompactionResult>;
|
|
24
35
|
model: ModelRef;
|
|
36
|
+
toolExecution: ToolExecutionMode;
|
|
37
|
+
cutoff: EntryId;
|
|
25
38
|
handle: DeferredHandle;
|
|
26
39
|
pollAt: number;
|
|
40
|
+
} | {
|
|
41
|
+
/** Waiting on the round's tool tasks, which the generation owns (spec §8.5). */
|
|
42
|
+
phase: "tools";
|
|
43
|
+
/** The tool-calling answer. */
|
|
44
|
+
assistant: EntryId;
|
|
45
|
+
/** Tool tasks created so far, in call order; grows by one per started call of a sequential round. */
|
|
46
|
+
tools: TaskId<ToolTaskResult>[];
|
|
47
|
+
/** Calls of a sequential round not started yet, in call order. */
|
|
48
|
+
pending: string[];
|
|
27
49
|
};
|
|
28
50
|
export type GenerationResult = {
|
|
29
51
|
entryId: EntryId;
|
|
30
52
|
};
|
|
31
53
|
/**
|
|
32
|
-
* Built-in generation task: prepares the positional system prompt, requests or polls the model,
|
|
33
|
-
* the response. The run's inputs live in `pi.live.run`.
|
|
54
|
+
* Built-in generation task: prepares the positional system prompt and tool loadout, requests or polls the model,
|
|
55
|
+
* retries, and classifies the response. The run's inputs live in `pi.live.run`.
|
|
56
|
+
*/
|
|
57
|
+
export declare const GenerationTask: import("../types.ts").Task<GenerationInput, GenerationCheckpoint, GenerationResult, GenerationHooks>;
|
|
58
|
+
/**
|
|
59
|
+
* Append a committed partial left by an interrupted, aborted, faulted, or orphaned attempt as an aborted assistant
|
|
60
|
+
* entry; the caller replaces or removes `generation`.
|
|
34
61
|
*/
|
|
35
|
-
export declare
|
|
62
|
+
export declare function convertPartial(tx: Tx, live: Draft<LiveState>, conversationId: ConversationId): Promise<void>;
|
|
63
|
+
/** Start a run for `inputs`, placed input submissions: a new generation takes `pi.live.run`. */
|
|
64
|
+
export declare function startRun(tx: Tx, conversationId: ConversationId, live: Draft<LiveState>, inputs: SubmissionId[]): Promise<void>;
|
|
65
|
+
/** Hand run control from `from` to `to`; the run's inputs move with it. */
|
|
66
|
+
export declare function handOver(live: Draft<LiveState>, from: TaskId, to: TaskId): void;
|
|
36
67
|
//# sourceMappingURL=generation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generation.d.ts","sourceRoot":"","sources":["../../src/harness/generation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"generation.d.ts","sourceRoot":"","sources":["../../src/harness/generation.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { copyJson } from "@earendil-works/chord";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { isContextOverflow } from "@earendil-works/pi-ai/utils/overflow";
|
|
3
|
+
import { isRetryableAssistantError, retryDelayMs } from "@earendil-works/pi-ai/utils/retry";
|
|
4
|
+
import { getCurrentTools } from "@earendil-works/pi-ai/utils/transcript";
|
|
5
|
+
import { AssistantEntry, ResetEntry, SystemEntry, UserEntry } from "../entries.js";
|
|
4
6
|
import { defineTask } from "../tasks.js";
|
|
5
|
-
import {
|
|
7
|
+
import { createCompaction, estimateContext, selectCut } from "./compaction.js";
|
|
8
|
+
import { ConversationConfig, DEFAULT_COMPACTION_POLICY, DEFAULT_RETRY_POLICY } from "./config.js";
|
|
9
|
+
import { applyBoundary, prepareBoundary } from "./inbox.js";
|
|
10
|
+
import { assignJson } from "./json.js";
|
|
6
11
|
import { endRun, LiveDoc } from "./live.js";
|
|
7
|
-
import { planSystemEntries, renderSections, replaySections } from "./prompt.js";
|
|
12
|
+
import { desiredTools, planSystemEntries, renderSections, replaySections } from "./prompt.js";
|
|
13
|
+
import { appendToolResult, harnessError, ToolTask } from "./tool.js";
|
|
14
|
+
import { recordUsage } from "./usage.js";
|
|
8
15
|
const PARTIAL_THROTTLE_MS = 100;
|
|
9
16
|
const DEFAULT_POLL_AFTER_MS = 5000;
|
|
10
17
|
/**
|
|
11
|
-
* Built-in generation task: prepares the positional system prompt, requests or polls the model,
|
|
12
|
-
* the response. The run's inputs live in `pi.live.run`.
|
|
18
|
+
* Built-in generation task: prepares the positional system prompt and tool loadout, requests or polls the model,
|
|
19
|
+
* retries, and classifies the response. The run's inputs live in `pi.live.run`.
|
|
13
20
|
*/
|
|
14
21
|
export const GenerationTask = defineTask({
|
|
15
22
|
name: "pi.generation",
|
|
@@ -17,24 +24,33 @@ export const GenerationTask = defineTask({
|
|
|
17
24
|
initial: () => ({ phase: "prepare", attempt: 1 }),
|
|
18
25
|
phases: {
|
|
19
26
|
/**
|
|
20
|
-
* Render the system prompt and append the positional `pi.system` entries
|
|
21
|
-
*
|
|
27
|
+
* Render the system prompt and tool loadout and append the positional `pi.system` entries they need, then move to
|
|
28
|
+
* `request`. The configuration read here is fixed for this request. Only the Harness writes to a busy
|
|
29
|
+
* conversation, so the transcript read here is still the tail at the commit.
|
|
22
30
|
*/
|
|
23
31
|
prepare: async (task, runtime, context) => {
|
|
24
32
|
const { conversationId, registry } = runtime;
|
|
25
33
|
for (const failure of registry.failures())
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const { model, thinkingLevel, streamOptions } = (await runtime.snapshot(ConversationConfig, conversationId, context)) ??
|
|
34
|
+
runtime.report(failure.error);
|
|
35
|
+
const config = (await runtime.snapshot(ConversationConfig, conversationId, context)) ??
|
|
29
36
|
ConversationConfig.definition.initial();
|
|
30
|
-
|
|
37
|
+
const { model, thinkingLevel, streamOptions } = config;
|
|
38
|
+
const resolved = model === undefined ? undefined : runtime.models.getModel(model.provider, model.modelId);
|
|
39
|
+
if (model === undefined || resolved === undefined)
|
|
31
40
|
return failNoModel(runtime, model, context);
|
|
41
|
+
const { attempt, compacted, overflow } = task.state.checkpoint;
|
|
42
|
+
if (compacted !== undefined && overflow !== undefined) {
|
|
43
|
+
const [outcome] = await runtime.outcomes([compacted], context);
|
|
44
|
+
if (outcome?.status !== "completed" || outcome.result.entryId === undefined) {
|
|
45
|
+
return failModelError(runtime, overflow, context);
|
|
46
|
+
}
|
|
32
47
|
}
|
|
33
48
|
const view = await runtime.context(conversationId, context);
|
|
34
49
|
const shown = replaySections(view.messages);
|
|
50
|
+
const tools = desiredTools(config.activeTools, (name) => registry.tool(name));
|
|
35
51
|
const input = {
|
|
36
52
|
conversationId,
|
|
37
|
-
tools
|
|
53
|
+
tools,
|
|
38
54
|
shown: Object.fromEntries(shown),
|
|
39
55
|
model,
|
|
40
56
|
thinkingLevel,
|
|
@@ -42,20 +58,43 @@ export const GenerationTask = defineTask({
|
|
|
42
58
|
};
|
|
43
59
|
const report = (error) => runtime.report(error);
|
|
44
60
|
const desired = await renderSections(registry.sections(), input, shown, report, context);
|
|
45
|
-
const entries = planSystemEntries(view, desired, runtime.now());
|
|
61
|
+
const entries = planSystemEntries(view, desired, tools, runtime.now());
|
|
62
|
+
const threshold = compacted === undefined
|
|
63
|
+
? thresholdCompaction(view, entries, resolved.contextWindow, config.compaction ?? DEFAULT_COMPACTION_POLICY)
|
|
64
|
+
: undefined;
|
|
65
|
+
if (threshold === "blocking") {
|
|
66
|
+
// Compact first and prepare again; the transcript is unchanged until the compaction appends.
|
|
67
|
+
await runtime.commit(async (tx) => {
|
|
68
|
+
const child = await createCompaction(tx, conversationId, { reason: "threshold" }, runtime.taskId);
|
|
69
|
+
const checkpoint = { phase: "prepare", attempt, compacted: child };
|
|
70
|
+
return { status: "waiting", checkpoint, on: [child], policy: "allSettled" };
|
|
71
|
+
}, context);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
46
74
|
await runtime.commit(async (tx) => {
|
|
47
75
|
let cutoff = (await tx.scanEntries({ conversationId }, 1)).items[0]?.id;
|
|
48
76
|
for (const entry of entries)
|
|
49
77
|
cutoff = (await tx.appendEntry(SystemEntry, conversationId, entry)).id;
|
|
50
78
|
if (cutoff === undefined)
|
|
51
79
|
throw new Error(`Conversation ${conversationId} has no entries to send`);
|
|
52
|
-
|
|
53
|
-
|
|
80
|
+
// Checked in this commit, so a compaction admitted during preparation counts.
|
|
81
|
+
if (threshold === "background" && (await tx.doc(LiveDoc, conversationId)).compactions === undefined) {
|
|
82
|
+
await createCompaction(tx, conversationId, { reason: "threshold" });
|
|
83
|
+
}
|
|
84
|
+
const request = {
|
|
85
|
+
attempt,
|
|
86
|
+
...(compacted === undefined ? {} : { compacted }),
|
|
87
|
+
model,
|
|
88
|
+
thinkingLevel,
|
|
89
|
+
streamOptions: streamOptions ?? {},
|
|
90
|
+
toolExecution: config.toolExecution ?? "parallel",
|
|
91
|
+
cutoff,
|
|
92
|
+
};
|
|
54
93
|
return { status: "running", checkpoint: { phase: "request", ...request } };
|
|
55
94
|
}, context);
|
|
56
95
|
},
|
|
57
96
|
request: async (task, runtime, context) => {
|
|
58
|
-
const { attempt, model: ref, thinkingLevel, streamOptions, cutoff } = task.state.checkpoint;
|
|
97
|
+
const { attempt, compacted, model: ref, thinkingLevel, streamOptions, toolExecution, cutoff, } = task.state.checkpoint;
|
|
59
98
|
const conversationId = runtime.conversationId;
|
|
60
99
|
await runtime.commit(async (tx) => {
|
|
61
100
|
const live = await tx.doc(LiveDoc, conversationId);
|
|
@@ -67,30 +106,64 @@ export const GenerationTask = defineTask({
|
|
|
67
106
|
if (model === undefined)
|
|
68
107
|
return failNoModel(runtime, ref, context);
|
|
69
108
|
const view = await runtime.context(conversationId, context, cutoff);
|
|
109
|
+
let messages = view.messages;
|
|
110
|
+
await runtime.hooks.each("beforeRequest", async (hook) => {
|
|
111
|
+
const replaced = await hook({ messages }, runtime, context);
|
|
112
|
+
if (replaced !== undefined)
|
|
113
|
+
messages = replaced.messages;
|
|
114
|
+
});
|
|
70
115
|
const options = {
|
|
71
116
|
...streamOptions,
|
|
72
117
|
signal: runtime.signal,
|
|
73
118
|
...(thinkingLevel === "off" ? {} : { reasoning: thinkingLevel }),
|
|
74
119
|
};
|
|
75
|
-
const message = await streamResponse(runtime, model,
|
|
76
|
-
|
|
120
|
+
const message = await streamResponse(runtime, model, messages, options, attempt, context);
|
|
121
|
+
const request = { attempt, compacted, model: ref, toolExecution, cutoff, messages: view.messages };
|
|
122
|
+
await classify(runtime, request, message, context);
|
|
77
123
|
},
|
|
78
124
|
retry: async (task, runtime, context) => {
|
|
79
|
-
const { attempt, until } = task.state.checkpoint;
|
|
125
|
+
const { attempt, compacted, until } = task.state.checkpoint;
|
|
80
126
|
await runtime.sleep(until, context);
|
|
81
127
|
await runtime.commit(async (tx) => {
|
|
82
128
|
(await tx.doc(LiveDoc, runtime.conversationId)).generation = { attempt: attempt + 1 };
|
|
83
|
-
|
|
129
|
+
const checkpoint = {
|
|
130
|
+
phase: "prepare",
|
|
131
|
+
attempt: attempt + 1,
|
|
132
|
+
...(compacted === undefined ? {} : { compacted }),
|
|
133
|
+
};
|
|
134
|
+
return { status: "running", checkpoint };
|
|
84
135
|
}, context);
|
|
85
136
|
},
|
|
86
137
|
poll: async (task, runtime, context) => {
|
|
87
|
-
const { attempt, model: ref, handle, pollAt } = task.state.checkpoint;
|
|
138
|
+
const { attempt, compacted, model: ref, toolExecution, cutoff, handle, pollAt } = task.state.checkpoint;
|
|
88
139
|
const model = runtime.models.getModel(ref.provider, ref.modelId);
|
|
89
140
|
if (model === undefined)
|
|
90
141
|
return failNoModel(runtime, ref, context);
|
|
91
142
|
await runtime.sleep(pollAt, context);
|
|
92
143
|
const message = await runtime.models.fetchDeferred(model, handle, { signal: runtime.signal });
|
|
93
|
-
|
|
144
|
+
const request = { attempt, compacted, model: ref, toolExecution, cutoff, pollAt };
|
|
145
|
+
await classify(runtime, request, message, context);
|
|
146
|
+
},
|
|
147
|
+
tools: async (task, runtime, context) => {
|
|
148
|
+
const { assistant, tools, pending } = task.state.checkpoint;
|
|
149
|
+
const [next, ...rest] = pending;
|
|
150
|
+
if (next === undefined)
|
|
151
|
+
return finishToolRound(runtime, assistant, tools, context);
|
|
152
|
+
// Sequential round: start the next call and wait for it.
|
|
153
|
+
await runtime.commit(async (tx) => {
|
|
154
|
+
const live = await tx.doc(LiveDoc, runtime.conversationId);
|
|
155
|
+
const taskId = await createToolTask(tx, runtime, assistant, next);
|
|
156
|
+
const slot = live.tools?.find((slot) => slot.callId === next && slot.taskId === undefined);
|
|
157
|
+
if (slot !== undefined)
|
|
158
|
+
slot.taskId = taskId;
|
|
159
|
+
const checkpoint = {
|
|
160
|
+
phase: "tools",
|
|
161
|
+
assistant,
|
|
162
|
+
tools: [...tools, taskId],
|
|
163
|
+
pending: rest,
|
|
164
|
+
};
|
|
165
|
+
return { status: "waiting", checkpoint, on: [taskId], policy: "allSettled" };
|
|
166
|
+
}, context);
|
|
94
167
|
},
|
|
95
168
|
},
|
|
96
169
|
abort: async (task, runtime, context) => {
|
|
@@ -107,14 +180,59 @@ export const GenerationTask = defineTask({
|
|
|
107
180
|
}
|
|
108
181
|
}
|
|
109
182
|
const conversationId = runtime.conversationId;
|
|
183
|
+
// Runs after the round's tool tasks are terminal; calls never started get `aborted` results (spec §8.5).
|
|
184
|
+
const unstarted = checkpoint.phase === "tools"
|
|
185
|
+
? await readCalls(runtime, checkpoint.assistant, checkpoint.pending, context)
|
|
186
|
+
: [];
|
|
110
187
|
await runtime.commit(async (tx) => {
|
|
111
188
|
const live = await tx.doc(LiveDoc, conversationId);
|
|
112
189
|
await convertPartial(tx, live, conversationId);
|
|
190
|
+
for (const call of unstarted) {
|
|
191
|
+
const result = harnessError("aborted", `Tool ${call.name} was aborted`);
|
|
192
|
+
await appendToolResult(tx, conversationId, call, result, runtime.now());
|
|
193
|
+
}
|
|
113
194
|
endRun(tx, live, runtime.taskId, { status: "unanswered", reason: "aborted" });
|
|
114
195
|
return { status: "terminal", outcome: { status: "aborted" } };
|
|
115
196
|
}, context);
|
|
116
197
|
},
|
|
117
198
|
});
|
|
199
|
+
/** The calls `callIds` of the assistant entry, in the given order. */
|
|
200
|
+
async function readCalls(runtime, assistant, callIds, context) {
|
|
201
|
+
const message = (await runtime.entry(AssistantEntry, assistant, context))?.model?.[0];
|
|
202
|
+
const calls = message?.role === "assistant" ? message.content.filter((content) => content.type === "toolCall") : [];
|
|
203
|
+
return callIds.flatMap((id) => calls.filter((call) => call.id === id).slice(0, 1));
|
|
204
|
+
}
|
|
205
|
+
/** A tool task for call `callId`, owned by the generation. */
|
|
206
|
+
function createToolTask(tx, runtime, assistant, callId) {
|
|
207
|
+
return tx.createTask(ToolTask, { assistant, callId }, { ownership: { kind: "task", taskId: runtime.taskId } });
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Which threshold compaction preparation starts before its request (spec §8.3): `blocking` above
|
|
211
|
+
* `contextWindow - reserveTokens`, `background` above the background threshold, and only when range selection finds a
|
|
212
|
+
* cut. The caller starts a background one only while no compaction is listed.
|
|
213
|
+
*/
|
|
214
|
+
function thresholdCompaction(view, planned, contextWindow, policy) {
|
|
215
|
+
if (!policy.enabled || contextWindow <= 0)
|
|
216
|
+
return undefined;
|
|
217
|
+
const tokens = estimateContext(view, planned.flatMap((entry) => entry.model ?? []));
|
|
218
|
+
const blocking = contextWindow - policy.reserveTokens;
|
|
219
|
+
const background = blocking - policy.backgroundTokens;
|
|
220
|
+
const over = tokens > blocking ? "blocking" : policy.backgroundTokens > 0 && tokens > background ? "background" : undefined;
|
|
221
|
+
if (over === undefined || selectCut(view, policy.keepRecentTokens) === undefined)
|
|
222
|
+
return undefined;
|
|
223
|
+
return over;
|
|
224
|
+
}
|
|
225
|
+
/** Settle the run's inputs `unanswered` with `model_error` and fail with `text`. */
|
|
226
|
+
async function failModelError(runtime, text, context) {
|
|
227
|
+
await runtime.commit(async (tx) => {
|
|
228
|
+
const live = await tx.doc(LiveDoc, runtime.conversationId);
|
|
229
|
+
endRun(tx, live, runtime.taskId, { status: "unanswered", reason: "model_error", detail: text });
|
|
230
|
+
return {
|
|
231
|
+
status: "terminal",
|
|
232
|
+
outcome: { status: "failed", error: { message: text, detail: { reason: "model_error" } } },
|
|
233
|
+
};
|
|
234
|
+
}, context);
|
|
235
|
+
}
|
|
118
236
|
/** Settle the run's inputs `unanswered` with `no_model` and fail. */
|
|
119
237
|
async function failNoModel(runtime, ref, context) {
|
|
120
238
|
const message = ref === undefined ? "No model is configured" : `Model ${ref.provider}/${ref.modelId} is not available`;
|
|
@@ -124,13 +242,16 @@ async function failNoModel(runtime, ref, context) {
|
|
|
124
242
|
return { status: "terminal", outcome: { status: "failed", error: { message, detail: { reason: "no_model" } } } };
|
|
125
243
|
}, context);
|
|
126
244
|
}
|
|
127
|
-
/**
|
|
128
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Append a committed partial left by an interrupted, aborted, faulted, or orphaned attempt as an aborted assistant
|
|
247
|
+
* entry; the caller replaces or removes `generation`.
|
|
248
|
+
*/
|
|
249
|
+
export async function convertPartial(tx, live, conversationId) {
|
|
129
250
|
const partial = live.generation?.message;
|
|
130
251
|
if (partial === undefined)
|
|
131
252
|
return;
|
|
132
253
|
const message = copyJson(partial);
|
|
133
|
-
await tx
|
|
254
|
+
await appendAssistant(tx, conversationId, { ...message, stopReason: "aborted" });
|
|
134
255
|
}
|
|
135
256
|
/**
|
|
136
257
|
* Stream one request and return the terminal message. Partials commit as trailing writes at most every 100 ms with one
|
|
@@ -171,7 +292,9 @@ async function streamResponse(runtime, model, messages, options, attempt, contex
|
|
|
171
292
|
try {
|
|
172
293
|
const events = runtime.models.streamSimple(model, { messages: [...messages] }, options);
|
|
173
294
|
for await (const event of events) {
|
|
174
|
-
|
|
295
|
+
// A partial without content, such as pi-ai's opening `start` event, shows nothing; a deferred response
|
|
296
|
+
// never gets past it, so it never leaves a partial.
|
|
297
|
+
if (event.type === "done" || event.type === "error" || event.partial.content.length === 0)
|
|
175
298
|
continue;
|
|
176
299
|
pending = event.partial;
|
|
177
300
|
if (timer === undefined && inFlight === undefined)
|
|
@@ -185,43 +308,78 @@ async function streamResponse(runtime, model, messages, options, attempt, contex
|
|
|
185
308
|
await inFlight;
|
|
186
309
|
}
|
|
187
310
|
}
|
|
188
|
-
/**
|
|
189
|
-
|
|
190
|
-
* message came from polling, so a still deferred result polls strictly later.
|
|
191
|
-
*/
|
|
192
|
-
async function classify(runtime, attempt, ref, previousPollAt, message, context) {
|
|
311
|
+
/** Classify a terminal provider message in one commit that also clears the partial. */
|
|
312
|
+
async function classify(runtime, request, message, context) {
|
|
193
313
|
// An abort mark or close: the abort invocation or the reopened run handles the committed state.
|
|
194
314
|
runtime.signal.throwIfAborted();
|
|
195
315
|
const conversationId = runtime.conversationId;
|
|
316
|
+
const { attempt, compacted, model: ref, toolExecution, cutoff } = request;
|
|
196
317
|
if (message.stopReason === "deferred" && message.deferred !== undefined) {
|
|
197
318
|
const handle = message.deferred;
|
|
198
|
-
const pollAt = Math.max(runtime.now() + (handle.pollAfterMs ?? DEFAULT_POLL_AFTER_MS),
|
|
319
|
+
const pollAt = Math.max(runtime.now() + (handle.pollAfterMs ?? DEFAULT_POLL_AFTER_MS), request.pollAt === undefined ? Number.NEGATIVE_INFINITY : request.pollAt + 1);
|
|
199
320
|
await runtime.commit(async (tx) => {
|
|
200
321
|
(await tx.doc(LiveDoc, conversationId)).generation = { attempt, deferred: { pollAt } };
|
|
201
|
-
|
|
322
|
+
const checkpoint = {
|
|
323
|
+
phase: "poll",
|
|
324
|
+
attempt,
|
|
325
|
+
...(compacted === undefined ? {} : { compacted }),
|
|
326
|
+
model: ref,
|
|
327
|
+
toolExecution,
|
|
328
|
+
cutoff,
|
|
329
|
+
handle,
|
|
330
|
+
pollAt,
|
|
331
|
+
};
|
|
332
|
+
return { status: "running", checkpoint };
|
|
202
333
|
}, context);
|
|
203
334
|
return;
|
|
204
335
|
}
|
|
205
|
-
|
|
336
|
+
await runtime.hooks.each("afterResponse", (hook) => hook(message, runtime, context));
|
|
337
|
+
const calls = message.content.filter((content) => content.type === "toolCall");
|
|
338
|
+
if (message.stopReason === "toolUse" && calls.length > 0) {
|
|
339
|
+
return startToolRound(runtime, request, message, calls, context);
|
|
340
|
+
}
|
|
341
|
+
if (message.stopReason === "stop" || message.stopReason === "length" || message.stopReason === "toolUse") {
|
|
342
|
+
return answer(runtime, message, context);
|
|
343
|
+
}
|
|
344
|
+
// The retry and compaction policies govern the next attempt, so they are read now rather than pinned at preparation.
|
|
345
|
+
const config = await runtime.snapshot(ConversationConfig, conversationId, context);
|
|
346
|
+
const overflow = message.stopReason === "error" && isContextOverflow(message);
|
|
347
|
+
if (overflow && compacted === undefined && (config?.compaction ?? DEFAULT_COMPACTION_POLICY).enabled) {
|
|
348
|
+
const policy = config?.compaction ?? DEFAULT_COMPACTION_POLICY;
|
|
349
|
+
const view = await runtime.context(conversationId, context, cutoff);
|
|
350
|
+
if (selectCut(view, policy.keepRecentTokens) !== undefined) {
|
|
351
|
+
const text = message.errorMessage ?? "Context overflow";
|
|
352
|
+
await runtime.commit(async (tx) => {
|
|
353
|
+
const live = await tx.doc(LiveDoc, conversationId);
|
|
354
|
+
await appendAssistant(tx, conversationId, message);
|
|
355
|
+
delete live.generation;
|
|
356
|
+
const child = await createCompaction(tx, conversationId, { reason: "overflow" }, runtime.taskId);
|
|
357
|
+
const checkpoint = { phase: "prepare", attempt, compacted: child, overflow: text };
|
|
358
|
+
return { status: "waiting", checkpoint, on: [child], policy: "allSettled" };
|
|
359
|
+
}, context);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const policy = config?.retry ?? DEFAULT_RETRY_POLICY;
|
|
364
|
+
// An overflow is never retried: only a compaction can make the next request fit.
|
|
206
365
|
const retry = message.stopReason === "error" &&
|
|
366
|
+
!overflow &&
|
|
207
367
|
isRetryableAssistantError(message) &&
|
|
208
368
|
policy.enabled &&
|
|
209
369
|
attempt <= policy.maxRetries;
|
|
210
370
|
const until = retry ? runtime.now() + retryDelayMs(policy, attempt) : 0;
|
|
211
371
|
await runtime.commit(async (tx) => {
|
|
212
372
|
const live = await tx.doc(LiveDoc, conversationId);
|
|
213
|
-
|
|
214
|
-
switch (message.stopReason) {
|
|
215
|
-
case "stop":
|
|
216
|
-
case "length":
|
|
217
|
-
// Until the tool chain exists, a tool call settles as the answer.
|
|
218
|
-
case "toolUse":
|
|
219
|
-
endRun(tx, live, runtime.taskId, { status: "done", answer: entry.id });
|
|
220
|
-
return { status: "terminal", outcome: { status: "completed", result: { entryId: entry.id } } };
|
|
221
|
-
}
|
|
373
|
+
await appendAssistant(tx, conversationId, message);
|
|
222
374
|
if (retry) {
|
|
223
375
|
live.generation = { attempt, retry: { at: until, error: message.errorMessage ?? "" } };
|
|
224
|
-
|
|
376
|
+
const checkpoint = {
|
|
377
|
+
phase: "retry",
|
|
378
|
+
attempt,
|
|
379
|
+
...(compacted === undefined ? {} : { compacted }),
|
|
380
|
+
until,
|
|
381
|
+
};
|
|
382
|
+
return { status: "running", checkpoint };
|
|
225
383
|
}
|
|
226
384
|
const text = message.errorMessage ?? `Model response ended with stop reason ${message.stopReason}`;
|
|
227
385
|
endRun(tx, live, runtime.taskId, { status: "unanswered", reason: "model_error", detail: text });
|
|
@@ -232,35 +390,155 @@ async function classify(runtime, attempt, ref, previousPollAt, message, context)
|
|
|
232
390
|
}, context);
|
|
233
391
|
}
|
|
234
392
|
/**
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
393
|
+
* A final answer; the final boundary places queued items (spec §6). The first `onYield` continuation appends a user
|
|
394
|
+
* message and hands the run to a successor generation, but only when the boundary selected no user item and no reset.
|
|
395
|
+
* Otherwise the run's inputs settle `done`, and selected user items start the next run.
|
|
238
396
|
*/
|
|
239
|
-
function
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
397
|
+
async function answer(runtime, message, context) {
|
|
398
|
+
let continuation;
|
|
399
|
+
await runtime.hooks.each("onYield", async (hook) => {
|
|
400
|
+
if (continuation !== undefined)
|
|
401
|
+
return;
|
|
402
|
+
continuation = (await hook(message, runtime, context))?.continue;
|
|
403
|
+
});
|
|
404
|
+
const conversationId = runtime.conversationId;
|
|
405
|
+
await runtime.commit(async (tx) => {
|
|
406
|
+
const boundary = await prepareBoundary(tx, conversationId);
|
|
407
|
+
const live = await tx.doc(LiveDoc, conversationId);
|
|
408
|
+
const entry = await appendAssistant(tx, conversationId, message);
|
|
409
|
+
const result = { status: "terminal", outcome: { status: "completed", result: { entryId: entry.id } } };
|
|
410
|
+
const { users, reset } = await applyBoundary(tx, boundary, "final", runtime.now());
|
|
411
|
+
if (continuation !== undefined && users.length === 0 && !reset) {
|
|
412
|
+
const user = { role: "user", content: continuation, timestamp: runtime.now() };
|
|
413
|
+
await tx.appendEntry(UserEntry, conversationId, { model: [user] });
|
|
414
|
+
handOver(live, runtime.taskId, await createGeneration(tx, conversationId));
|
|
415
|
+
delete live.generation;
|
|
416
|
+
return result;
|
|
257
417
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
418
|
+
endRun(tx, live, runtime.taskId, { status: "done", answer: entry.id });
|
|
419
|
+
if (users.length > 0)
|
|
420
|
+
await startRun(tx, conversationId, live, users);
|
|
421
|
+
return result;
|
|
422
|
+
}, context);
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Append the tool-calling answer and start its tool round in one commit (spec §8.3). A call to a tool the request did
|
|
426
|
+
* not offer gets its `tool_unavailable` result here; every other call gets a tool task owned by the generation, only the
|
|
427
|
+
* first one now when the round is sequential. The generation then waits for them in its `tools` phase, keeping the run.
|
|
428
|
+
*/
|
|
429
|
+
async function startToolRound(runtime, request, message, calls, context) {
|
|
430
|
+
const conversationId = runtime.conversationId;
|
|
431
|
+
const messages = request.messages ?? (await runtime.context(conversationId, context, request.cutoff)).messages;
|
|
432
|
+
const offered = new Set(getCurrentTools(messages).map((tool) => tool.name));
|
|
433
|
+
const sequential = request.toolExecution === "sequential" ||
|
|
434
|
+
calls.some((call) => offered.has(call.name) && runtime.registry.tool(call.name)?.executionMode === "sequential");
|
|
435
|
+
await runtime.commit(async (tx) => {
|
|
436
|
+
const live = await tx.doc(LiveDoc, conversationId);
|
|
437
|
+
const entry = await appendAssistant(tx, conversationId, message);
|
|
438
|
+
const slots = [];
|
|
439
|
+
const tools = [];
|
|
440
|
+
const pending = [];
|
|
441
|
+
for (const call of calls) {
|
|
442
|
+
if (!offered.has(call.name)) {
|
|
443
|
+
const unavailable = harnessError("tool_unavailable", `Tool ${call.name} is not available`);
|
|
444
|
+
const result = await appendToolResult(tx, conversationId, call, unavailable, runtime.now());
|
|
445
|
+
slots.push({ callId: call.id, name: call.name, status: "done", entry: result.id });
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (sequential && tools.length > 0) {
|
|
449
|
+
pending.push(call.id);
|
|
450
|
+
slots.push({ callId: call.id, name: call.name, status: "pending" });
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
const taskId = await createToolTask(tx, runtime, entry.id, call.id);
|
|
454
|
+
tools.push(taskId);
|
|
455
|
+
slots.push({ callId: call.id, name: call.name, taskId, status: "pending" });
|
|
456
|
+
}
|
|
457
|
+
delete live.generation;
|
|
458
|
+
live.tools = slots;
|
|
459
|
+
const checkpoint = { phase: "tools", assistant: entry.id, tools, pending };
|
|
460
|
+
return { status: "waiting", checkpoint, on: tools, policy: "allSettled" };
|
|
461
|
+
}, context);
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* The round's tools are terminal: apply their controls and either end the run at the final boundary (`terminate`,
|
|
465
|
+
* `handoff`, or a queued reset) or hand it to the next generation at the `postTools` boundary (spec §8.5).
|
|
466
|
+
*/
|
|
467
|
+
async function finishToolRound(runtime, assistant, tools, context) {
|
|
468
|
+
const conversationId = runtime.conversationId;
|
|
469
|
+
const controls = new Map();
|
|
470
|
+
const outcomes = await runtime.outcomes(tools, context);
|
|
471
|
+
tools.forEach((id, index) => {
|
|
472
|
+
const outcome = outcomes[index];
|
|
473
|
+
controls.set(id, outcome.status === "completed" ? outcome.result.control : undefined);
|
|
474
|
+
});
|
|
475
|
+
const slots = (await runtime.snapshot(LiveDoc, conversationId, context))?.tools ?? [];
|
|
476
|
+
const results = slots.flatMap((slot) => (slot.entry === undefined ? [] : [slot.entry]));
|
|
477
|
+
await runtime.hooks.each("afterTools", (hook) => hook(assistant, results, runtime, context));
|
|
478
|
+
// Every call of the round, including those answered without a task, must ask to terminate.
|
|
479
|
+
const terminate = slots.length > 0 &&
|
|
480
|
+
slots.every((slot) => slot.taskId !== undefined && controls.get(slot.taskId)?.terminate === true);
|
|
481
|
+
const added = [...controls.values()].flatMap((control) => control?.addTools ?? []);
|
|
482
|
+
// The last handoff in call order wins.
|
|
483
|
+
const handoff = [...controls.values()].findLast((control) => control?.handoff !== undefined)?.handoff;
|
|
484
|
+
await runtime.commit(async (tx) => {
|
|
485
|
+
const boundary = await prepareBoundary(tx, conversationId);
|
|
486
|
+
if (added.length > 0) {
|
|
487
|
+
const config = await tx.doc(ConversationConfig, conversationId);
|
|
488
|
+
for (const name of added)
|
|
489
|
+
if (!config.activeTools.includes(name))
|
|
490
|
+
config.activeTools.push(name);
|
|
491
|
+
}
|
|
492
|
+
const live = await tx.doc(LiveDoc, conversationId);
|
|
493
|
+
const now = runtime.now();
|
|
494
|
+
if (terminate || handoff !== undefined) {
|
|
495
|
+
if (handoff !== undefined) {
|
|
496
|
+
const message = { role: "user", content: handoff, timestamp: now };
|
|
497
|
+
const entry = await tx.appendEntry(ResetEntry, conversationId, { head: "self", model: [message] });
|
|
498
|
+
boundary.head = entry.id;
|
|
499
|
+
}
|
|
500
|
+
const { users } = await applyBoundary(tx, boundary, "final", now);
|
|
501
|
+
endRun(tx, live, runtime.taskId, { status: "done", answer: assistant });
|
|
502
|
+
if (users.length > 0)
|
|
503
|
+
await startRun(tx, conversationId, live, users);
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
const { users, reset } = await applyBoundary(tx, boundary, "postTools", now);
|
|
507
|
+
if (reset) {
|
|
508
|
+
// The queued reset cut the run's context before an answer.
|
|
509
|
+
endRun(tx, live, runtime.taskId, { status: "unanswered", reason: "reset" });
|
|
510
|
+
if (users.length > 0)
|
|
511
|
+
await startRun(tx, conversationId, live, users);
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
delete live.tools;
|
|
515
|
+
if (live.run?.taskId === runtime.taskId)
|
|
516
|
+
live.run.inputs.push(...users);
|
|
517
|
+
handOver(live, runtime.taskId, await createGeneration(tx, conversationId));
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return { status: "terminal", outcome: { status: "completed", result: { entryId: assistant } } };
|
|
521
|
+
}, context);
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Append a provider result and add its usage to `pi.usage` in the same commit.
|
|
525
|
+
* REMINDER: every built-in writer of assistant entries goes through here, so the usage ledger stays complete.
|
|
526
|
+
*/
|
|
527
|
+
async function appendAssistant(tx, conversationId, message) {
|
|
528
|
+
await recordUsage(tx, conversationId, "models", `${message.provider}/${message.model}`, message.usage);
|
|
529
|
+
return tx.appendEntry(AssistantEntry, conversationId, { model: [message] });
|
|
530
|
+
}
|
|
531
|
+
/** Start a run for `inputs`, placed input submissions: a new generation takes `pi.live.run`. */
|
|
532
|
+
export async function startRun(tx, conversationId, live, inputs) {
|
|
533
|
+
live.run = { taskId: await createGeneration(tx, conversationId), inputs };
|
|
534
|
+
}
|
|
535
|
+
/** A generation owned by its conversation. */
|
|
536
|
+
function createGeneration(tx, conversationId) {
|
|
537
|
+
return tx.createTask(GenerationTask, {}, { ownership: { kind: "conversation" }, conversationId });
|
|
262
538
|
}
|
|
263
|
-
|
|
264
|
-
|
|
539
|
+
/** Hand run control from `from` to `to`; the run's inputs move with it. */
|
|
540
|
+
export function handOver(live, from, to) {
|
|
541
|
+
if (live.run?.taskId === from)
|
|
542
|
+
live.run.taskId = to;
|
|
265
543
|
}
|
|
266
544
|
//# sourceMappingURL=generation.js.map
|