@inflexa-ai/harness 0.41.1 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/conversation-agent.js +3 -0
- package/dist/agents/report-session-agent.d.ts +2 -2
- package/dist/agents/report-session-agent.js +4 -0
- package/dist/app/chat-turn.d.ts +0 -4
- package/dist/app/chat-turn.js +39 -25
- package/dist/app/message-assembly.d.ts +2 -2
- package/dist/app/message-assembly.js +1 -1
- package/dist/contracts/chat-events.d.ts +12 -5
- package/dist/contracts/chat-events.js +3 -3
- package/dist/contracts/chat-frame.d.ts +40 -0
- package/dist/contracts/chat-frame.js +166 -0
- package/dist/contracts/chat-parts.d.ts +15 -3
- package/dist/contracts/chat-parts.js +2 -2
- package/dist/contracts/index.d.ts +6 -4
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/message.d.ts +7 -5
- package/dist/contracts/message.js +1 -1
- package/dist/contracts/part-registry.d.ts +6 -6
- package/dist/contracts/report-blocks.d.ts +10 -10
- package/dist/contracts/report-reference.d.ts +4 -4
- package/dist/contracts/schemas/chat-events.d.ts +2 -2
- package/dist/contracts/schemas/chat-events.js +2 -2
- package/dist/contracts/schemas/chat-parts.d.ts +34 -2
- package/dist/contracts/schemas/chat-parts.js +9 -3
- package/dist/contracts/schemas/index.d.ts +2 -2
- package/dist/contracts/schemas/index.js +2 -2
- package/dist/execution/run-event-parts.d.ts +4 -4
- package/dist/execution/run-event-parts.js +0 -0
- package/dist/execution/run-event-stream.d.ts +2 -2
- package/dist/index.d.ts +9 -6
- package/dist/index.js +3 -2
- package/dist/lib/page-capture.d.ts +27 -9
- package/dist/lib/page-capture.js +81 -33
- package/dist/loop/compaction.d.ts +8 -3
- package/dist/loop/run-agent.d.ts +11 -0
- package/dist/loop/run-agent.js +62 -35
- package/dist/loop/types.d.ts +3 -0
- package/dist/memory/ai-sdk-message-storage.d.ts +18 -3
- package/dist/memory/ai-sdk-message-storage.js +23 -1
- package/dist/memory/conversation-display-recorder.d.ts +7 -1
- package/dist/memory/conversation-display-recorder.js +59 -119
- package/dist/memory/conversation-display-replay.d.ts +3 -3
- package/dist/memory/conversation-display-replay.js +7 -21
- package/dist/memory/conversation-display-storage.d.ts +10 -10
- package/dist/memory/conversation-display-storage.js +9 -8
- package/dist/memory/conversation-view.d.ts +5 -4
- package/dist/memory/conversation-view.js +14 -18
- package/dist/memory/count-tokens.js +1 -0
- package/dist/memory/message-backfill.d.ts +8 -2
- package/dist/memory/thread-store.d.ts +9 -2
- package/dist/memory/thread-store.js +8 -2
- package/dist/prompts/compaction.d.ts +2 -2
- package/dist/prompts/compaction.js +2 -2
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +12 -0
- package/dist/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +40 -28
- package/dist/report-model/authoring-grammar.d.ts +4 -4
- package/dist/report-render/views/block-mark.d.ts +8 -0
- package/dist/report-render/views/block-mark.js +10 -0
- package/dist/report-render/views/chart-view.js +2 -1
- package/dist/report-render/views/prose.js +10 -6
- package/dist/report-render/views/values.js +5 -4
- package/dist/runtime/boot.d.ts +2 -2
- package/dist/runtime/boot.js +3 -3
- package/dist/state/init.d.ts +7 -6
- package/dist/state/init.js +51 -672
- package/dist/state/migrations/20260927120000_baseline.d.ts +11 -0
- package/dist/state/migrations/20260927120000_baseline.js +675 -0
- package/dist/state/migrations/20260927180000_thread_title_set_by_user.d.ts +6 -0
- package/dist/state/migrations/20260927180000_thread_title_set_by_user.js +10 -0
- package/dist/state/migrations/index.d.ts +10 -0
- package/dist/state/migrations/index.js +15 -0
- package/dist/tools/display/show-user.js +6 -4
- package/dist/tools/report-authoring/authoring-tools.d.ts +16 -5
- package/dist/tools/report-authoring/authoring-tools.js +52 -24
- package/dist/tools/report-session/examine-page.d.ts +29 -14
- package/dist/tools/report-session/examine-page.js +40 -16
- package/dist/tools/report-session/list-artifacts.d.ts +23 -10
- package/dist/tools/report-session/list-artifacts.js +51 -31
- package/dist/tools/report-session/page-faults.d.ts +29 -0
- package/dist/tools/report-session/page-faults.js +48 -0
- package/dist/tools/report-session/preview-report.d.ts +9 -2
- package/dist/tools/report-session/preview-report.js +13 -3
- package/dist/tools/report-session/record-version.d.ts +5 -1
- package/dist/tools/report-session/record-version.js +4 -3
- package/dist/tools/workspace/show-file.js +4 -3
- package/package.json +2 -1
|
@@ -1,33 +1,36 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { applyChatFrame, checkChatPart, isRootFrame, toChatFrame } from "../contracts/chat-frame.js";
|
|
2
3
|
import { PART_REGISTRY } from "../contracts/part-registry.js";
|
|
3
|
-
import { CortexChatPartSchema } from "../contracts/schemas/chat-parts.js";
|
|
4
4
|
import { compactionExchangeOf, compactionMarkerOf } from "./ai-sdk-message-storage.js";
|
|
5
5
|
import { conversationDisplayPart } from "./conversation-display-storage.js";
|
|
6
6
|
function jsonCopy(value) {
|
|
7
7
|
return JSON.parse(JSON.stringify(value));
|
|
8
8
|
}
|
|
9
|
-
function isSubAgent(event, topLevelCallPath) {
|
|
10
|
-
if (!("source" in event) || event.source === undefined)
|
|
11
|
-
return false;
|
|
12
|
-
const path = event.source.callPath;
|
|
13
|
-
return path.length > topLevelCallPath.length && topLevelCallPath.every((entry, index) => path[index] === entry);
|
|
14
|
-
}
|
|
15
9
|
function durableConversationType(type) {
|
|
16
10
|
if (!Object.hasOwn(PART_REGISTRY, type))
|
|
17
11
|
return false;
|
|
18
12
|
const descriptor = PART_REGISTRY[type];
|
|
19
13
|
return descriptor.emitter === "conversation" && descriptor.consumer === "conversation" && !descriptor.transient;
|
|
20
14
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
15
|
+
/** The checked part of a data frame, copied at receipt, because the emitter keeps its own reference to the payload. */
|
|
16
|
+
function recordedPart(frame) {
|
|
17
|
+
const checked = checkChatPart(frame);
|
|
18
|
+
if (!checked.ok)
|
|
19
|
+
throw new Error(`Invalid emitted conversation part ${frame.type}: ${checked.error}`);
|
|
20
|
+
return jsonCopy(checked.frame);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The stored form of the parts of one round. A round that closes ends each question that it asked, thus a
|
|
24
|
+
* pending ask is stored `aborted`, and a call with no outcome is stored `incomplete` ({@link conversationDisplayPart}).
|
|
25
|
+
*/
|
|
26
|
+
function displayParts(parts) {
|
|
27
|
+
return parts.flatMap((part) => {
|
|
28
|
+
if (part.type === "text")
|
|
29
|
+
return part.text.length === 0 ? [] : [{ type: "text", text: part.text, state: "done" }];
|
|
30
|
+
if (part.type === "data-ask" && part.status === "pending")
|
|
31
|
+
return [conversationDisplayPart({ ...part, status: "aborted" })];
|
|
32
|
+
return [conversationDisplayPart(part)];
|
|
33
|
+
});
|
|
31
34
|
}
|
|
32
35
|
/** The text of the assistant messages of a round. The text of an exchange is a summary, and no reply. */
|
|
33
36
|
function assistantText(messages) {
|
|
@@ -48,148 +51,85 @@ function compactionDivider(marker) {
|
|
|
48
51
|
id: marker.id,
|
|
49
52
|
status: marker.kind === "summary" ? "done" : "failed",
|
|
50
53
|
tokensBefore: marker.tokensBefore,
|
|
51
|
-
tokensAfter: marker.tokensAfter,
|
|
52
54
|
durationMs: marker.durationMs,
|
|
55
|
+
...(marker.trigger === undefined ? {} : { trigger: marker.trigger }),
|
|
53
56
|
});
|
|
54
57
|
return { id: marker.id, role: "system", parts: [part] };
|
|
55
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Record the display of a turn through the same translation path as a live surface: each event becomes a
|
|
61
|
+
* frame (`toChatFrame`), and the frames build the messages of the round (`applyChatFrame`). Only a part that
|
|
62
|
+
* the conversation keeps enters the record.
|
|
63
|
+
*/
|
|
56
64
|
export function createConversationDisplayRecorder(options) {
|
|
57
65
|
// One id for each message of the turn, thus the rounds of a turn replay as one assistant message.
|
|
58
66
|
const userMessageId = options.userMessageId ?? randomUUID();
|
|
59
67
|
let assistantMessageId = options.assistantMessageId ?? randomUUID();
|
|
60
|
-
const
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
// legitimately share one.
|
|
64
|
-
const reconcileIndexes = new Map();
|
|
68
|
+
const rootSource = { agentId: options.agentId, callPath: [options.agentId] };
|
|
69
|
+
// The messages of the open round. A later update of a part lands in the next round, and the replay replaces the earlier copy.
|
|
70
|
+
let round = [];
|
|
65
71
|
let finished = false;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
*/
|
|
72
|
-
function upsert(part, reconciling) {
|
|
73
|
-
if (!reconciling || part.id === undefined) {
|
|
74
|
-
assistantParts.push(part);
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
const key = `${part.type}:${part.id}`;
|
|
78
|
-
const index = reconcileIndexes.get(key);
|
|
79
|
-
if (index !== undefined) {
|
|
80
|
-
assistantParts[index] = part;
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
reconcileIndexes.set(key, assistantParts.length);
|
|
84
|
-
assistantParts.push(part);
|
|
72
|
+
function apply(frame) {
|
|
73
|
+
round = applyChatFrame(round, frame, assistantMessageId).messages;
|
|
74
|
+
}
|
|
75
|
+
function roundParts() {
|
|
76
|
+
return round.find((message) => message.id === assistantMessageId)?.parts ?? [];
|
|
85
77
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
*
|
|
89
|
-
* A call is recorded `incomplete` the moment it is dispatched and overwritten with
|
|
90
|
-
* its real outcome when it finishes. The record is therefore honest at every
|
|
91
|
-
* instant, and a turn that ends mid-call needs no closing pass — the calls that
|
|
92
|
-
* never finished are already saying so.
|
|
93
|
-
*/
|
|
94
|
-
function recordToolCall(toolCallId, toolName, outcome, detail) {
|
|
95
|
-
upsert(conversationDisplayPart({
|
|
96
|
-
type: "tool-call",
|
|
97
|
-
toolCallId,
|
|
98
|
-
toolName,
|
|
99
|
-
outcome,
|
|
100
|
-
...(detail === undefined ? {} : { detail }),
|
|
101
|
-
}), true);
|
|
78
|
+
function hasText() {
|
|
79
|
+
return roundParts().some((part) => part.type === "text" && part.text.length > 0);
|
|
102
80
|
}
|
|
103
81
|
function appendText(text) {
|
|
104
|
-
if (text.length
|
|
82
|
+
if (text.length > 0)
|
|
83
|
+
apply({ type: "text-delta", text, source: rootSource });
|
|
84
|
+
}
|
|
85
|
+
function record(event) {
|
|
86
|
+
const frame = toChatFrame(event, rootSource);
|
|
87
|
+
if (frame === null || !isRootFrame(frame))
|
|
105
88
|
return;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
89
|
+
if (frame.type.startsWith("data-")) {
|
|
90
|
+
// Each chat event type is named without the `data-` prefix, thus a `data-` frame is a part frame.
|
|
91
|
+
if (durableConversationType(frame.type))
|
|
92
|
+
apply(recordedPart(frame));
|
|
109
93
|
return;
|
|
110
94
|
}
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
function recordData(event) {
|
|
114
|
-
if (!durableConversationType(event.type))
|
|
115
|
-
return;
|
|
116
|
-
upsert(dataPart(event), PART_REGISTRY[event.type].reconciling);
|
|
95
|
+
apply(frame);
|
|
117
96
|
}
|
|
118
97
|
const emit = (event) => {
|
|
119
|
-
if (finished)
|
|
120
|
-
|
|
121
|
-
if (!isSubAgent(event, options.topLevelCallPath)) {
|
|
122
|
-
switch (event.type) {
|
|
123
|
-
case "text-delta":
|
|
124
|
-
appendText(event.text);
|
|
125
|
-
break;
|
|
126
|
-
// The detail the loop computed at dispatch is recorded as shown. It is
|
|
127
|
-
// NOT re-derived on reload: doing that would need the tool's schema and
|
|
128
|
-
// the raw input, which is the tool-name coupling the display projection
|
|
129
|
-
// exists to remove.
|
|
130
|
-
case "tool-started":
|
|
131
|
-
recordToolCall(event.toolUseId, event.name, "incomplete", event.detail);
|
|
132
|
-
break;
|
|
133
|
-
case "tool-finished":
|
|
134
|
-
recordToolCall(event.toolUseId, event.name, event.outcome, event.detail);
|
|
135
|
-
break;
|
|
136
|
-
case "done":
|
|
137
|
-
case "iteration":
|
|
138
|
-
break;
|
|
139
|
-
default:
|
|
140
|
-
recordData(event);
|
|
141
|
-
break;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
98
|
+
if (!finished)
|
|
99
|
+
record(event);
|
|
144
100
|
return options.sink(event);
|
|
145
101
|
};
|
|
146
|
-
function closeParts() {
|
|
147
|
-
// Calls need no closing pass: one that never reached `tool-finished` still holds
|
|
148
|
-
// the `incomplete` it was recorded with at dispatch, which is the whole of what
|
|
149
|
-
// the harness observed. An approval does need one — `pending` is a question, and
|
|
150
|
-
// a turn that ends without answering it aborted it.
|
|
151
|
-
for (const part of assistantParts) {
|
|
152
|
-
if (part.type === "data-ask" && part.data.status === "pending")
|
|
153
|
-
part.data.status = "aborted";
|
|
154
|
-
if (part.type === "text")
|
|
155
|
-
part.state = "done";
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
102
|
function takeOpening() {
|
|
159
103
|
return [{ id: userMessageId, role: "user", parts: [{ type: "text", text: options.userText, state: "done" }] }];
|
|
160
104
|
}
|
|
161
105
|
function takeRound(messages) {
|
|
162
|
-
if (!
|
|
106
|
+
if (!hasText())
|
|
163
107
|
appendText(assistantText(messages));
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
// A later update of a part lands in the next round, and the replay replaces the earlier copy.
|
|
168
|
-
assistantParts.length = 0;
|
|
169
|
-
reconcileIndexes.clear();
|
|
108
|
+
const parts = displayParts(roundParts());
|
|
109
|
+
const taken = parts.length === 0 ? [] : [{ id: assistantMessageId, role: "assistant", parts: jsonCopy(parts) }];
|
|
110
|
+
round = [];
|
|
170
111
|
const marker = messages.map(compactionMarkerOf).find((found) => found !== undefined);
|
|
171
112
|
if (marker !== undefined) {
|
|
172
|
-
|
|
113
|
+
taken.push(compactionDivider(marker));
|
|
173
114
|
// The divider splits the turn into two assistant messages of the replay, and two messages must not share an id.
|
|
174
115
|
assistantMessageId = randomUUID();
|
|
175
116
|
}
|
|
176
|
-
return
|
|
117
|
+
return taken;
|
|
177
118
|
}
|
|
178
119
|
function finish(finishOptions) {
|
|
179
120
|
if (!finished) {
|
|
180
121
|
finished = true;
|
|
181
|
-
if (!
|
|
122
|
+
if (!hasText() && finishOptions?.fallbackText)
|
|
182
123
|
appendText(finishOptions.fallbackText);
|
|
183
|
-
}
|
|
184
|
-
closeParts();
|
|
185
124
|
}
|
|
186
125
|
const messages = takeOpening();
|
|
187
|
-
|
|
126
|
+
const parts = displayParts(roundParts());
|
|
127
|
+
if (parts.length > 0) {
|
|
188
128
|
messages.push({
|
|
189
129
|
id: assistantMessageId,
|
|
190
130
|
role: "assistant",
|
|
191
131
|
...(finishOptions?.interrupted ? { metadata: { interrupted: true } } : {}),
|
|
192
|
-
parts: jsonCopy(
|
|
132
|
+
parts: jsonCopy(parts),
|
|
193
133
|
});
|
|
194
134
|
}
|
|
195
135
|
return messages;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The runtime transcript read: stored display projections → `
|
|
2
|
+
* The runtime transcript read: stored display projections → `ChatMessage[]`. It walks the rows in
|
|
3
3
|
* `seq` order, merges the rounds of a turn into one assistant message, and folds the turn record onto
|
|
4
4
|
* that message. It consults no tool name, no card builder, and no workspace or database.
|
|
5
5
|
*
|
|
6
6
|
* A row with no envelope predates the display projection and is skipped. A display rebuilt from the
|
|
7
7
|
* model transcript would let each later change of a tool or a card rewrite history.
|
|
8
8
|
*/
|
|
9
|
-
import type {
|
|
9
|
+
import type { ChatMessage } from "../contracts/message.js";
|
|
10
10
|
import type { StoredMessage } from "./thread-history.js";
|
|
11
|
-
export declare function
|
|
11
|
+
export declare function storedMessagesToChat(messages: readonly StoredMessage[]): ChatMessage[];
|
|
@@ -1,32 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The runtime transcript read: stored display projections → `
|
|
2
|
+
* The runtime transcript read: stored display projections → `ChatMessage[]`. It walks the rows in
|
|
3
3
|
* `seq` order, merges the rounds of a turn into one assistant message, and folds the turn record onto
|
|
4
4
|
* that message. It consults no tool name, no card builder, and no workspace or database.
|
|
5
5
|
*
|
|
6
6
|
* A row with no envelope predates the display projection and is skipped. A display rebuilt from the
|
|
7
7
|
* model transcript would let each later change of a tool or a card rewrite history.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { upsertPart } from "../contracts/chat-frame.js";
|
|
10
10
|
import { isSyntheticUserMessage } from "./ai-sdk-message-storage.js";
|
|
11
|
-
import {
|
|
12
|
-
/** The key that a part reconciles on, or `undefined` for a part that each round adds again. */
|
|
13
|
-
function reconcileKey(part) {
|
|
14
|
-
if (part.type === "tool-call")
|
|
15
|
-
return `tool-call:${part.toolCallId}`;
|
|
16
|
-
if (part.type === "text" || !isReconciling(part.type))
|
|
17
|
-
return undefined;
|
|
18
|
-
return "id" in part && typeof part.id === "string" ? `${part.type}:${part.id}` : undefined;
|
|
19
|
-
}
|
|
11
|
+
import { conversationUIToChatMessages } from "./conversation-display-storage.js";
|
|
20
12
|
/** Add the parts of a later round to the assistant message of its turn. */
|
|
21
13
|
function mergeRound(target, round) {
|
|
22
|
-
for (const part of round.parts)
|
|
23
|
-
|
|
24
|
-
const earlier = key === undefined ? -1 : target.parts.findIndex((candidate) => reconcileKey(candidate) === key);
|
|
25
|
-
if (earlier >= 0)
|
|
26
|
-
target.parts[earlier] = part;
|
|
27
|
-
else
|
|
28
|
-
target.parts.push(part);
|
|
29
|
-
}
|
|
14
|
+
for (const part of round.parts)
|
|
15
|
+
target.parts = upsertPart(target.parts, part);
|
|
30
16
|
}
|
|
31
17
|
function foldTurnRecord(message, record) {
|
|
32
18
|
if (message === undefined || record === undefined)
|
|
@@ -38,7 +24,7 @@ function foldTurnRecord(message, record) {
|
|
|
38
24
|
if (record.status === "aborted")
|
|
39
25
|
message.interrupted = true;
|
|
40
26
|
}
|
|
41
|
-
export function
|
|
27
|
+
export function storedMessagesToChat(messages) {
|
|
42
28
|
const out = [];
|
|
43
29
|
// The last message of the group being walked, as `out` holds it.
|
|
44
30
|
let groupLast;
|
|
@@ -62,7 +48,7 @@ export function storedMessagesToCortex(messages) {
|
|
|
62
48
|
const createdAt = row.createdAt === undefined ? {} : { createdAt: row.createdAt.toISOString() };
|
|
63
49
|
const author = row.author === undefined ? {} : { author: row.author };
|
|
64
50
|
groupLast = undefined;
|
|
65
|
-
for (const message of
|
|
51
|
+
for (const message of conversationUIToChatMessages(row.displayEnvelope.messages)) {
|
|
66
52
|
const stamped = { ...message, ...createdAt, ...(message.role === "user" ? author : {}) };
|
|
67
53
|
const previous = out.at(-1);
|
|
68
54
|
// The merged message keeps the creation time of its first round.
|
|
@@ -4,20 +4,20 @@
|
|
|
4
4
|
*
|
|
5
5
|
* AI SDK `UIMessage` is the CONTAINER: it supplies message identity, role,
|
|
6
6
|
* ordering, metadata, and `validateUIMessages` for runtime validation. The PARTS
|
|
7
|
-
* vocabulary is
|
|
7
|
+
* vocabulary is the harness's own, because ours is the richer one — a `MessagePart`
|
|
8
8
|
* carries a tool call's four-way outcome and its one-line detail, and AI SDK's
|
|
9
9
|
* `dynamic-tool` part has nowhere to put either. Mapping through `dynamic-tool`
|
|
10
10
|
* would silently drop both on every reload, so every non-text part rides as a
|
|
11
|
-
* typed `DataUIPart` whose payload IS the
|
|
11
|
+
* typed `DataUIPart` whose payload IS the message part minus its discriminant.
|
|
12
12
|
*
|
|
13
13
|
* The projection is therefore lossless in both directions, and the read path
|
|
14
|
-
* ({@link
|
|
14
|
+
* ({@link conversationUIToChatMessages}) is a near-identity rather than a
|
|
15
15
|
* reconstruction.
|
|
16
16
|
*/
|
|
17
17
|
import { type DataUIPart, type UIMessage } from "ai";
|
|
18
18
|
import type { AskPart, ChildSessionStartedPart, CompactionPart, FileReferencePart, PlanPart, PresentationPart, ReportRenderedPart, RunCardPart } from "../contracts/chat-parts.js";
|
|
19
19
|
import type { ToolCallOutcome } from "../contracts/chat-events.js";
|
|
20
|
-
import type {
|
|
20
|
+
import type { ChatMessage, MessagePart, ToolCallPart } from "../contracts/message.js";
|
|
21
21
|
import type { Logger } from "../lib/logger.js";
|
|
22
22
|
export declare const SUPPORTED_DISPLAY_SCHEMA_VERSION = 1;
|
|
23
23
|
type Payload<T extends {
|
|
@@ -26,7 +26,7 @@ type Payload<T extends {
|
|
|
26
26
|
/**
|
|
27
27
|
* The AI SDK custom-data vocabulary persisted for one conversation display.
|
|
28
28
|
*
|
|
29
|
-
* Each key `k` becomes a `data-${k}` part whose payload is the matching
|
|
29
|
+
* Each key `k` becomes a `data-${k}` part whose payload is the matching message
|
|
30
30
|
* part without its `type` field, so a stored part and a wire part differ only in
|
|
31
31
|
* where the discriminant lives.
|
|
32
32
|
*/
|
|
@@ -56,24 +56,24 @@ export interface StoredDisplayEnvelope {
|
|
|
56
56
|
export declare function envelopeDisplayMessages(messages: readonly ConversationUIMessage[]): StoredDisplayEnvelope;
|
|
57
57
|
export declare function parseStoredDisplayEnvelope(value: unknown, identity: string, logger?: Logger): Promise<StoredDisplayEnvelope>;
|
|
58
58
|
/**
|
|
59
|
-
* Build the stored form of one non-text
|
|
59
|
+
* Build the stored form of one non-text message part.
|
|
60
60
|
*
|
|
61
|
-
* A tool call is the one part whose stored payload is not simply the
|
|
61
|
+
* A tool call is the one part whose stored payload is not simply the message part
|
|
62
62
|
* minus its discriminant: `outcome` is optional on the live part (absent means in
|
|
63
63
|
* flight) and required here, because a record is of a call that is no longer
|
|
64
64
|
* running. An in-flight call being recorded is one the turn never saw finish, so
|
|
65
65
|
* absence resolves to `incomplete` at exactly this boundary — once, where the
|
|
66
66
|
* lifetime changes — rather than at each reader.
|
|
67
67
|
*/
|
|
68
|
-
export declare function conversationDisplayPart(part: Exclude<
|
|
68
|
+
export declare function conversationDisplayPart(part: Exclude<MessagePart, {
|
|
69
69
|
type: "text";
|
|
70
70
|
}>): DataUIPart<ConversationUIData>;
|
|
71
71
|
/**
|
|
72
|
-
* Read the durable representation back as
|
|
72
|
+
* Read the durable representation back as chat messages.
|
|
73
73
|
*
|
|
74
74
|
* Every part round-trips by moving its discriminant back out of the part type,
|
|
75
75
|
* so nothing here consults a tool name, a registry, or the filesystem. A message
|
|
76
76
|
* whose parts all render empty is dropped so the transcript has no empty bubbles.
|
|
77
77
|
*/
|
|
78
|
-
export declare function
|
|
78
|
+
export declare function conversationUIToChatMessages(messages: readonly ConversationUIMessage[]): ChatMessage[];
|
|
79
79
|
export {};
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
*
|
|
5
5
|
* AI SDK `UIMessage` is the CONTAINER: it supplies message identity, role,
|
|
6
6
|
* ordering, metadata, and `validateUIMessages` for runtime validation. The PARTS
|
|
7
|
-
* vocabulary is
|
|
7
|
+
* vocabulary is the harness's own, because ours is the richer one — a `MessagePart`
|
|
8
8
|
* carries a tool call's four-way outcome and its one-line detail, and AI SDK's
|
|
9
9
|
* `dynamic-tool` part has nowhere to put either. Mapping through `dynamic-tool`
|
|
10
10
|
* would silently drop both on every reload, so every non-text part rides as a
|
|
11
|
-
* typed `DataUIPart` whose payload IS the
|
|
11
|
+
* typed `DataUIPart` whose payload IS the message part minus its discriminant.
|
|
12
12
|
*
|
|
13
13
|
* The projection is therefore lossless in both directions, and the read path
|
|
14
|
-
* ({@link
|
|
14
|
+
* ({@link conversationUIToChatMessages}) is a near-identity rather than a
|
|
15
15
|
* reconstruction.
|
|
16
16
|
*/
|
|
17
17
|
import { validateUIMessages } from "ai";
|
|
@@ -44,6 +44,7 @@ const ToolCallDisplaySchema = z.object({
|
|
|
44
44
|
toolName: z.string(),
|
|
45
45
|
outcome: ToolCallOutcomeSchema,
|
|
46
46
|
detail: z.string().optional(),
|
|
47
|
+
durationMs: z.number().optional(),
|
|
47
48
|
});
|
|
48
49
|
/**
|
|
49
50
|
* The schema behind each stored part key.
|
|
@@ -152,7 +153,7 @@ export async function parseStoredDisplayEnvelope(value, identity, logger) {
|
|
|
152
153
|
throw new Error(`Invalid stored conversation display envelope at ${identity}: ${message}`, { cause: error });
|
|
153
154
|
}
|
|
154
155
|
}
|
|
155
|
-
/** The data-part key a
|
|
156
|
+
/** The data-part key a message part is stored under — its `type` minus the `data-` prefix. */
|
|
156
157
|
function displayPartType(part) {
|
|
157
158
|
return (part.type === "tool-call" ? "data-tool-call" : part.type);
|
|
158
159
|
}
|
|
@@ -163,9 +164,9 @@ function displayPartId(part) {
|
|
|
163
164
|
return "id" in part && typeof part.id === "string" ? part.id : undefined;
|
|
164
165
|
}
|
|
165
166
|
/**
|
|
166
|
-
* Build the stored form of one non-text
|
|
167
|
+
* Build the stored form of one non-text message part.
|
|
167
168
|
*
|
|
168
|
-
* A tool call is the one part whose stored payload is not simply the
|
|
169
|
+
* A tool call is the one part whose stored payload is not simply the message part
|
|
169
170
|
* minus its discriminant: `outcome` is optional on the live part (absent means in
|
|
170
171
|
* flight) and required here, because a record is of a call that is no longer
|
|
171
172
|
* running. An in-flight call being recorded is one the turn never saw finish, so
|
|
@@ -178,13 +179,13 @@ export function conversationDisplayPart(part) {
|
|
|
178
179
|
return { type: displayPartType(part), id: displayPartId(part), data };
|
|
179
180
|
}
|
|
180
181
|
/**
|
|
181
|
-
* Read the durable representation back as
|
|
182
|
+
* Read the durable representation back as chat messages.
|
|
182
183
|
*
|
|
183
184
|
* Every part round-trips by moving its discriminant back out of the part type,
|
|
184
185
|
* so nothing here consults a tool name, a registry, or the filesystem. A message
|
|
185
186
|
* whose parts all render empty is dropped so the transcript has no empty bubbles.
|
|
186
187
|
*/
|
|
187
|
-
export function
|
|
188
|
+
export function conversationUIToChatMessages(messages) {
|
|
188
189
|
const out = [];
|
|
189
190
|
for (const message of messages) {
|
|
190
191
|
const parts = [];
|
|
@@ -47,7 +47,8 @@ export interface ConversationView {
|
|
|
47
47
|
export declare function conversationView(messages: readonly ModelMessage[], options: ConversationViewOptions): ConversationView;
|
|
48
48
|
/** The message with no reasoning part, or `undefined` for an assistant message with no part left. */
|
|
49
49
|
export declare function withoutReasoning(message: ModelMessage): ModelMessage | undefined;
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
/**
|
|
51
|
+
* The input tokens that the provider reported for the last request of the conversation. A marker changes the
|
|
52
|
+
* conversation, thus a request before the latest marker gives `undefined`.
|
|
53
|
+
*/
|
|
54
|
+
export declare function lastRequestInputTokens(messages: readonly ModelMessage[]): number | undefined;
|
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
* turns of the body without their reasoning. The loop and the reader call this one function, thus the
|
|
5
5
|
* next turn reads back the prefix that the last request sent.
|
|
6
6
|
*/
|
|
7
|
-
import { compactionExchangeOf, compactionMarkerOf, isSyntheticUserMessage } from "./ai-sdk-message-storage.js";
|
|
8
|
-
import { countTokens } from "./count-tokens.js";
|
|
7
|
+
import { compactionExchangeOf, compactionMarkerOf, isSyntheticUserMessage, requestInputTokensOf } from "./ai-sdk-message-storage.js";
|
|
9
8
|
/**
|
|
10
9
|
* A turn starts on a `user` message that a human actually sent. Two kinds of
|
|
11
10
|
* message carry the `user` role without opening a turn, and both are excluded
|
|
@@ -110,21 +109,18 @@ export function withoutReasoning(message) {
|
|
|
110
109
|
return message;
|
|
111
110
|
return content.length === 0 ? undefined : { ...message, content };
|
|
112
111
|
}
|
|
113
|
-
/**
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
break;
|
|
126
|
-
total += turnTokens[t];
|
|
127
|
-
kept++;
|
|
112
|
+
/**
|
|
113
|
+
* The input tokens that the provider reported for the last request of the conversation. A marker changes the
|
|
114
|
+
* conversation, thus a request before the latest marker gives `undefined`.
|
|
115
|
+
*/
|
|
116
|
+
export function lastRequestInputTokens(messages) {
|
|
117
|
+
for (let at = messages.length - 1; at >= 0; at--) {
|
|
118
|
+
const message = messages[at];
|
|
119
|
+
if (compactionMarkerOf(message) !== undefined)
|
|
120
|
+
return undefined;
|
|
121
|
+
const tokens = requestInputTokensOf(message);
|
|
122
|
+
if (tokens !== undefined)
|
|
123
|
+
return tokens;
|
|
128
124
|
}
|
|
129
|
-
return
|
|
125
|
+
return undefined;
|
|
130
126
|
}
|
|
@@ -1,2 +1,8 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type { QueryResultRow } from "pg";
|
|
2
|
+
/** The query method of a `pg` client, which the migration transaction also supplies. */
|
|
3
|
+
export interface BackfillClient {
|
|
4
|
+
query<R extends QueryResultRow>(text: string, values?: unknown[]): Promise<{
|
|
5
|
+
rows: R[];
|
|
6
|
+
}>;
|
|
7
|
+
}
|
|
8
|
+
export declare function backfillAiSdkMessageEnvelopes(client: BackfillClient): Promise<void>;
|
|
@@ -193,10 +193,17 @@ export interface ThreadStore {
|
|
|
193
193
|
/** The live thread by id, or `null` if absent or archived. */
|
|
194
194
|
getThread(threadId: string): ResultAsync<Thread | null, DbError>;
|
|
195
195
|
/**
|
|
196
|
-
*
|
|
197
|
-
*
|
|
196
|
+
* A rename by a person. Set the title and `title_set_by_user`, thus no
|
|
197
|
+
* automatic title replaces it. Bumps `updated_at` forward — never behind the
|
|
198
|
+
* stamp the row already carries. No-op on a missing/archived row.
|
|
198
199
|
*/
|
|
199
200
|
updateTitle(threadId: string, title: string): ResultAsync<Thread | null, DbError>;
|
|
201
|
+
/**
|
|
202
|
+
* An automatic title. Set the title only while no person set one, with the
|
|
203
|
+
* same forward-only bump as `updateTitle`. `null` when the row is missing,
|
|
204
|
+
* archived, or titled by a person.
|
|
205
|
+
*/
|
|
206
|
+
setAutoTitle(threadId: string, title: string): ResultAsync<Thread | null, DbError>;
|
|
200
207
|
/**
|
|
201
208
|
* Soft-delete the subtree: stamp `deleted_at` on the named thread and on
|
|
202
209
|
* every descendant reachable through `parent_thread_id`, at any depth, so a
|
|
@@ -220,10 +220,16 @@ export function createThreadStore(pool) {
|
|
|
220
220
|
// give that: it reads this transaction's START time, not the moment the
|
|
221
221
|
// row is actually written.
|
|
222
222
|
`UPDATE cortex_analysis_threads
|
|
223
|
-
SET title = $2, updated_at = GREATEST(updated_at, clock_timestamp())
|
|
223
|
+
SET title = $2, title_set_by_user = true, updated_at = GREATEST(updated_at, clock_timestamp())
|
|
224
224
|
WHERE thread_id = $1 AND deleted_at IS NULL
|
|
225
225
|
RETURNING ${THREAD_COLUMNS}`, [threadId, title])).map(({ rows }) => (rows[0] ? toThread(rows[0]) : null));
|
|
226
226
|
}
|
|
227
|
+
function setAutoTitle(threadId, title) {
|
|
228
|
+
return tryMutation("thread-store.setAutoTitle", () => pool.query(`UPDATE cortex_analysis_threads
|
|
229
|
+
SET title = $2, updated_at = GREATEST(updated_at, clock_timestamp())
|
|
230
|
+
WHERE thread_id = $1 AND deleted_at IS NULL AND NOT title_set_by_user
|
|
231
|
+
RETURNING ${THREAD_COLUMNS}`, [threadId, title])).map(({ rows }) => (rows[0] ? toThread(rows[0]) : null));
|
|
232
|
+
}
|
|
227
233
|
function archiveThread(threadId) {
|
|
228
234
|
return tryMutation("thread-store.archiveThread", () => pool.query(
|
|
229
235
|
// The stamp reaches the whole subtree because a hidden thread must
|
|
@@ -360,5 +366,5 @@ export function createThreadStore(pool) {
|
|
|
360
366
|
}));
|
|
361
367
|
});
|
|
362
368
|
}
|
|
363
|
-
return { createThread, getThread, updateTitle, archiveThread, unarchiveThread, purgeThread, listThreads };
|
|
369
|
+
return { createThread, getThread, updateTitle, setAutoTitle, archiveThread, unarchiveThread, purgeThread, listThreads };
|
|
364
370
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/** The compaction request of a thread whose agent maintains the working memory. */
|
|
2
|
-
export declare const MEMORY_COMPACTION_REQUEST = "The conversation is too long for the context window. The harness replaces the conversation above with your summary.\n\nFirst, move each lasting fact into the working memory with update_working_memory: the goal, each constraint, each hypothesis, and each finding with its run id. Keep each constraint that the user gave as it is, because the user set it. No other tool can run now. Send all the memory edits in at most 3 replies, and put many calls in one reply. The reply after the edits must be the summary: if it calls a tool, the harness gets no summary.\n\nThen reply in plain text with the summary. Leave out each fact that a memory edit accepted. If the memory refused an edit, put that fact in the summary. Give:\n- the
|
|
2
|
+
export declare const MEMORY_COMPACTION_REQUEST = "The conversation is too long for the context window. The harness replaces the conversation above with your summary.\n\nFirst, move each lasting fact into the working memory with update_working_memory: the goal, each constraint, each hypothesis, and each finding with its run id. Keep each constraint that the user gave as it is, because the user set it. No other tool can run now. Send all the memory edits in at most 3 replies, and put many calls in one reply. The reply after the edits must be the summary: if it calls a tool, the harness gets no summary.\n\nThen reply in plain text with the summary. Leave out each fact that a memory edit accepted. If the memory refused an edit, put that fact in the summary. Give:\n- the last request of the user in its exact words\n- the decisions, with their reasons\n- the approaches that failed or that the user ruled out, with the reasons\n- the open questions\n- the file paths, the run ids, and the other exact values that the work uses\n- the state of the work in progress\n\nThe conversation continues from your summary, the working memory, and the messages after them.";
|
|
3
3
|
/** The compaction request of a thread that reads a frozen copy of the working memory, for example a report thread. */
|
|
4
|
-
export declare const SUMMARY_COMPACTION_REQUEST = "The conversation is too long for the context window. The harness replaces the conversation above with your summary. No tool can run now.\n\nReply in plain text with the summary. Leave out what the working memory holds. Give:\n- the goals, and the
|
|
4
|
+
export declare const SUMMARY_COMPACTION_REQUEST = "The conversation is too long for the context window. The harness replaces the conversation above with your summary. No tool can run now.\n\nReply in plain text with the summary. Leave out what the working memory holds. Give:\n- the goals, and the last request of the user in its exact words\n- each change that the user made to the brief\n- the decisions, with their reasons\n- the approaches that failed or that the user ruled out, with the reasons\n- the open questions\n- the file paths, the run ids, and the other exact values that the work uses\n- the state of the work in progress\n\nThe conversation continues from your summary, the working memory, and the messages after them.";
|
|
@@ -5,7 +5,7 @@ export const MEMORY_COMPACTION_REQUEST = `The conversation is too long for the c
|
|
|
5
5
|
First, move each lasting fact into the working memory with update_working_memory: the goal, each constraint, each hypothesis, and each finding with its run id. Keep each constraint that the user gave as it is, because the user set it. No other tool can run now. Send all the memory edits in at most ${COMPACTION_MAX_REQUESTS - 1} replies, and put many calls in one reply. The reply after the edits must be the summary: if it calls a tool, the harness gets no summary.
|
|
6
6
|
|
|
7
7
|
Then reply in plain text with the summary. Leave out each fact that a memory edit accepted. If the memory refused an edit, put that fact in the summary. Give:
|
|
8
|
-
- the
|
|
8
|
+
- the last request of the user in its exact words
|
|
9
9
|
- the decisions, with their reasons
|
|
10
10
|
- the approaches that failed or that the user ruled out, with the reasons
|
|
11
11
|
- the open questions
|
|
@@ -17,7 +17,7 @@ The conversation continues from your summary, the working memory, and the messag
|
|
|
17
17
|
export const SUMMARY_COMPACTION_REQUEST = `The conversation is too long for the context window. The harness replaces the conversation above with your summary. No tool can run now.
|
|
18
18
|
|
|
19
19
|
Reply in plain text with the summary. Leave out what the working memory holds. Give:
|
|
20
|
-
- the goals, and the
|
|
20
|
+
- the goals, and the last request of the user in its exact words
|
|
21
21
|
- each change that the user made to the brief
|
|
22
22
|
- the decisions, with their reasons
|
|
23
23
|
- the approaches that failed or that the user ruled out, with the reasons
|