@jini-ai/chat 0.6.6 → 0.6.10
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 +129 -1
- package/dist/core/accepted-input.d.ts +54 -0
- package/dist/core/accepted-input.js +165 -0
- package/dist/core/agent-plugin-chat-state.d.ts +43 -0
- package/dist/core/agent-plugin-chat-state.js +48 -0
- package/dist/core/assistant-client/a2ui-action-poster.js +32 -28
- package/dist/core/assistant-client/messages.d.ts +1 -1
- package/dist/core/assistant-client/messages.js +2 -2
- package/dist/core/assistant-client/transport.d.ts +2 -2
- package/dist/core/assistant-client/transport.js +187 -98
- package/dist/core/assistant-content.d.ts +2 -1
- package/dist/core/assistant-content.js +9 -11
- package/dist/core/browser/page-actions.js +4 -2
- package/dist/core/composer-history.d.ts +16 -9
- package/dist/core/composer-history.js +28 -16
- package/dist/core/conversation-title.d.ts +3 -0
- package/dist/core/conversation-title.js +3 -0
- package/dist/core/durable-projection.js +20 -13
- package/dist/core/durable-runs/continuation.d.ts +4 -0
- package/dist/core/durable-runs/continuation.js +31 -6
- package/dist/core/durable-runs/ledger-contracts.d.ts +2 -1
- package/dist/core/durable-runs/ports.d.ts +6 -2
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/messages.d.ts +2 -0
- package/dist/core/partial-json.d.ts +6 -7
- package/dist/core/partial-json.js +81 -47
- package/dist/core/persistence/title.js +4 -2
- package/dist/core/question-answer.d.ts +23 -0
- package/dist/core/question-answer.js +66 -0
- package/dist/core/question-form/format.d.ts +6 -6
- package/dist/core/question-form/format.js +16 -17
- package/dist/core/question-form/normalize.d.ts +18 -0
- package/dist/core/question-form/normalize.js +112 -122
- package/dist/core/question-form/scan.js +141 -203
- package/dist/core/run-activity.js +62 -49
- package/dist/core/run-events/entry.d.ts +10 -1
- package/dist/core/run-events/entry.js +117 -87
- package/dist/core/todos.d.ts +1 -1
- package/dist/core/todos.js +60 -85
- package/dist/core/tool-events.js +14 -12
- package/dist/core/transcript.js +62 -16
- package/dist/core/turn-usage.js +54 -32
- package/dist/core/user-text-redaction.js +3 -0
- package/dist/core/util/manifest.js +77 -82
- package/dist/core/util/markdown-context.js +51 -53
- package/dist/core/util/parser.js +136 -144
- package/dist/core/util/pointer.js +16 -16
- package/dist/core/util/strip.js +57 -67
- package/dist/core/util/validate.js +15 -17
- package/dist/embed-widget/browser/session-store.js +21 -12
- package/dist/embed-widget/transports/fetch-sse.js +108 -95
- package/dist/embed-widget/vanilla/index.js +81 -70
- package/dist/react/agent-bridge/frontend-session-bridge.js +80 -52
- package/dist/react/components/AttachmentPreviewModal.js +25 -1
- package/dist/react/components/ChatSettingsDialog.d.ts +13 -0
- package/dist/react/components/ChatSettingsDialog.hooks.d.ts +115 -0
- package/dist/react/components/ChatSettingsDialog.hooks.js +40 -0
- package/dist/react/components/ChatSettingsDialog.js +72 -0
- package/dist/react/components/ChatSettingsSegmented.d.ts +18 -0
- package/dist/react/components/ChatSettingsSegmented.js +11 -0
- package/dist/react/components/Composer.d.ts +5 -1
- package/dist/react/components/Composer.js +126 -82
- package/dist/react/components/ConversationList.js +25 -39
- package/dist/react/components/ConversationTitleEditor.d.ts +27 -0
- package/dist/react/components/ConversationTitleEditor.js +54 -0
- package/dist/react/components/Icon.d.ts +1 -1
- package/dist/react/components/Icon.js +25 -45
- package/dist/react/components/Markdown.js +134 -123
- package/dist/react/components/McpUiSurfaceCard.d.ts +2 -2
- package/dist/react/components/McpUiSurfaceCard.js +30 -9
- package/dist/react/components/MessageList.d.ts +13 -13
- package/dist/react/components/MessageList.hooks.d.ts +6 -1
- package/dist/react/components/MessageList.hooks.js +19 -3
- package/dist/react/components/MessageList.js +34 -10
- package/dist/react/components/MessageRow.d.ts +1 -1
- package/dist/react/components/MessageRow.js +75 -47
- package/dist/react/components/MessageRow.usage.js +16 -11
- package/dist/react/components/MidRunUserMessage.js +16 -3
- package/dist/react/components/QuestionForm.js +148 -76
- package/dist/react/components/QuestionsPanel.js +1 -1
- package/dist/react/components/TodoCard.js +12 -6
- package/dist/react/components/ToolCallGroup.d.ts +4 -3
- package/dist/react/components/ToolCallGroup.js +7 -5
- package/dist/react/components/ToolCard.js +47 -40
- package/dist/react/components/composer-discovery.js +6 -1
- package/dist/react/components/mcp-ui-surface-state.d.ts +6 -11
- package/dist/react/components/mcp-ui-surface-state.js +23 -8
- package/dist/react/ext-event-renderer-registry.d.ts +8 -0
- package/dist/react/ext-event-renderer-registry.js +5 -2
- package/dist/react/features/chat-pane/components/AgentPluginUsage.d.ts +7 -0
- package/dist/react/features/chat-pane/components/AgentPluginUsage.js +45 -0
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +55 -19
- package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
- package/dist/react/features/chat-pane/components/ChatPane.js +192 -74
- package/dist/react/features/chat-pane/components/RuntimeDropUp.d.ts +29 -0
- package/dist/react/features/chat-pane/components/RuntimeDropUp.js +45 -0
- package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +80 -47
- package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.js +44 -40
- package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +6 -6
- package/dist/react/features/chat-pane/create-mid-run-message-poster.js +8 -8
- package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +3 -3
- package/dist/react/features/chat-pane/create-typed-answer-poster.js +3 -3
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.d.ts +6 -1
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +11 -5
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +12 -7
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +340 -149
- package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +7 -6
- package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +28 -10
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.d.ts +3 -3
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +60 -12
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.d.ts +49 -0
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +143 -0
- package/dist/react/features/chat-pane/model-options.d.ts +14 -0
- package/dist/react/features/chat-pane/model-options.js +45 -25
- package/dist/react/features/chat-pane/rules.d.ts +8 -9
- package/dist/react/features/chat-pane/rules.js +84 -46
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +152 -36
- package/dist/react/features/chat-pane/types.d.ts +19 -9
- package/dist/react/hooks/assistant-chats.js +12 -5
- package/dist/react/hooks/composer-draft-cache.js +116 -118
- package/dist/react/hooks/composer-history-storage.js +5 -5
- package/dist/react/hooks/fab-position.js +7 -4
- package/dist/react/hooks/useAnsweredCards.d.ts +21 -4
- package/dist/react/hooks/useAnsweredCards.js +38 -11
- package/dist/react/hooks/useArtifactStream.js +28 -22
- package/dist/react/hooks/useComposer.d.ts +20 -0
- package/dist/react/hooks/useComposer.js +22 -9
- package/dist/react/hooks/useComposerHistory.hooks.d.ts +6 -1
- package/dist/react/hooks/useComposerHistory.hooks.js +39 -19
- package/dist/react/hooks/useComposerHistoryKeys.hooks.js +24 -14
- package/dist/react/hooks/useConversation.d.ts +7 -0
- package/dist/react/hooks/useConversation.js +23 -10
- package/dist/react/hooks/useExtEventGroups.js +39 -39
- package/dist/react/hooks/useQuestionForms.js +21 -26
- package/dist/react/hooks/useRunStream.js +10 -2
- package/dist/react/index.d.ts +7 -1
- package/dist/react/index.js +9 -0
- package/dist/react/message-blocks.d.ts +6 -4
- package/dist/react/message-blocks.js +94 -72
- package/dist/react/slots.d.ts +2 -2
- package/dist/react/tool-call-display.d.ts +12 -6
- package/dist/react/tool-call-display.js +53 -24
- package/dist/server/durable-runs/recover.js +132 -80
- package/dist/server/run-finalizer/entry.js +25 -15
- package/dist/store/legacy/sqlite/conversations.d.ts +11 -11
- package/dist/store/legacy/sqlite/conversations.js +69 -98
- package/dist/store/legacy/sqlite/messages.d.ts +10 -2
- package/dist/store/legacy/sqlite/messages.js +113 -71
- package/dist/store/runtime/chat-history-store.js +9 -3
- package/dist/store/runtime/durable-run-store.d.ts +2 -2
- package/dist/store/runtime/durable-run-store.js +165 -117
- package/dist/store/runtime/history-tools.js +118 -82
- package/dist/store/runtime/run-ledger.d.ts +4 -1
- package/dist/store/runtime/run-ledger.js +56 -16
- package/dist/store/sql/cursor.js +38 -18
- package/dist/store/sql/store.d.ts +18 -0
- package/dist/store/sql/store.js +46 -15
- package/package.json +18 -16
|
@@ -37,25 +37,38 @@ function capMessage({ view }, _options = {}) {
|
|
|
37
37
|
}
|
|
38
38
|
return view;
|
|
39
39
|
}
|
|
40
|
+
function toolOutcome({ isError }, _options = {}) {
|
|
41
|
+
if (isError === true)
|
|
42
|
+
return 'failed';
|
|
43
|
+
return isError === false ? 'succeeded' : 'unknown';
|
|
44
|
+
}
|
|
45
|
+
/** Outcome per tool-use id; malformed historical events are ignored. */
|
|
46
|
+
function toolOutcomes({ events }, _options = {}) {
|
|
47
|
+
const outcomes = new Map();
|
|
48
|
+
for (const raw of events) {
|
|
49
|
+
const event = raw;
|
|
50
|
+
if (event?.kind === 'tool_result' && typeof event.toolUseId === 'string')
|
|
51
|
+
outcomes.set(event.toolUseId, toolOutcome({ isError: event.isError }));
|
|
52
|
+
}
|
|
53
|
+
return outcomes;
|
|
54
|
+
}
|
|
55
|
+
/** Well-formed tool-use events in stored order; malformed historical events are ignored. */
|
|
56
|
+
function toolUses({ events }, _options = {}) {
|
|
57
|
+
return events.filter((raw) => {
|
|
58
|
+
const event = raw;
|
|
59
|
+
return event?.kind === 'tool_use' && typeof event.name === 'string';
|
|
60
|
+
});
|
|
61
|
+
}
|
|
40
62
|
/** Only bounded names and status cross this boundary; malformed historical events are ignored.
|
|
41
63
|
* @complexity O(e) time/space in persisted events; output is capped at 50 summaries.
|
|
42
64
|
*/
|
|
43
65
|
function summarizeTools({ events, redact }, _options = {}) {
|
|
44
66
|
if (!Array.isArray(events))
|
|
45
67
|
return { items: [], truncated: false };
|
|
46
|
-
const outcomes =
|
|
47
|
-
for (const raw of events) {
|
|
48
|
-
const event = raw;
|
|
49
|
-
if (event?.kind === 'tool_result' && typeof event.toolUseId === 'string') {
|
|
50
|
-
outcomes.set(event.toolUseId, event.isError === true ? 'failed' : event.isError === false ? 'succeeded' : 'unknown');
|
|
51
|
-
}
|
|
52
|
-
}
|
|
68
|
+
const outcomes = toolOutcomes({ events });
|
|
53
69
|
const items = [];
|
|
54
70
|
let truncated = false;
|
|
55
|
-
for (const
|
|
56
|
-
const event = raw;
|
|
57
|
-
if (event?.kind !== 'tool_use' || typeof event.name !== 'string')
|
|
58
|
-
continue;
|
|
71
|
+
for (const event of toolUses({ events })) {
|
|
59
72
|
if (items.length === TOOL_EVENT_CAP) {
|
|
60
73
|
truncated = true;
|
|
61
74
|
break;
|
|
@@ -82,6 +95,98 @@ function inputRecord({ input, keys }, _options = {}) {
|
|
|
82
95
|
throw new ToolInputError({ message: 'Unexpected chat history input field. Use the published schema.' });
|
|
83
96
|
return record;
|
|
84
97
|
}
|
|
98
|
+
/** The scoped store's bounded reader; hosts without one fail closed for both tools. */
|
|
99
|
+
async function openHistory({ ports, principalId }, _options = {}) {
|
|
100
|
+
const store = await ports.getStore({ principalId }, {});
|
|
101
|
+
if (!store.history)
|
|
102
|
+
throw new ToolInputError({ message: 'Chat history is unavailable on this site.' });
|
|
103
|
+
return { store, history: store.history };
|
|
104
|
+
}
|
|
105
|
+
function listInput({ input: raw }, _options = {}) {
|
|
106
|
+
const input = inputRecord({ input: raw, keys: ['query', 'limit'] });
|
|
107
|
+
if (input.query !== undefined && (typeof input.query !== 'string' || input.query.length > 200))
|
|
108
|
+
throw new ToolInputError({ message: 'query must be a string of at most 200 characters.' });
|
|
109
|
+
const limit = integerOption({ input, key: 'limit', fallback: 20, max: 50 });
|
|
110
|
+
return { ...(input.query === undefined ? {} : { query: input.query }), limit };
|
|
111
|
+
}
|
|
112
|
+
function projectChatRow({ row, redact }, _options = {}) {
|
|
113
|
+
// Redact BEFORE slicing: a cap must not turn a credential into an undetectable fragment.
|
|
114
|
+
const title = redact({ text: row.title ?? '' }).text;
|
|
115
|
+
const first = redact({ text: row.firstUserLine }).text.split(/\r?\n/, 1)[0];
|
|
116
|
+
const truncated = title.length > 300 || first.length > 500 || row.id.length > 128;
|
|
117
|
+
return { truncated, chat: { id: redact({ text: row.id }).text.slice(0, 128), title: title.slice(0, 300), updatedAt: row.updatedAt,
|
|
118
|
+
messageCount: row.messageCount, firstUserLine: first.slice(0, 500) } };
|
|
119
|
+
}
|
|
120
|
+
async function listChats({ ports, redact, ctx }, _options = {}) {
|
|
121
|
+
const options = listInput({ input: ctx.input });
|
|
122
|
+
const { history } = await openHistory({ ports, principalId: ctx.principal.id });
|
|
123
|
+
const projected = (await history.list({}, options)).map(row => projectChatRow({ row, redact }));
|
|
124
|
+
const chats = projected.map(item => item.chat);
|
|
125
|
+
let truncated = projected.some(item => item.truncated);
|
|
126
|
+
// List is bounded too; serialized escaping can make text larger than its character count.
|
|
127
|
+
while (JSON.stringify({ chats, truncated }).length > HISTORY_TOTAL_CHAR_CAP) {
|
|
128
|
+
chats.pop();
|
|
129
|
+
truncated = true;
|
|
130
|
+
}
|
|
131
|
+
return { chats, truncated };
|
|
132
|
+
}
|
|
133
|
+
function readInput({ input: raw }, _options = {}) {
|
|
134
|
+
const input = inputRecord({ input: raw, keys: ['chatId', 'fromPosition', 'limit', 'includeToolEvents'] });
|
|
135
|
+
if (typeof input.chatId !== 'string' || !input.chatId.trim() || input.chatId.length > 128)
|
|
136
|
+
throw new ToolInputError({ message: 'chatId must be an id returned by assistant_chats_list.' });
|
|
137
|
+
const limit = integerOption({ input, key: 'limit', fallback: 50, max: 100 });
|
|
138
|
+
const fromPosition = input.fromPosition === undefined ? undefined : integerOption({ input, key: 'fromPosition', fallback: 0, max: Number.MAX_SAFE_INTEGER, min: 0 });
|
|
139
|
+
if (input.includeToolEvents !== undefined && typeof input.includeToolEvents !== 'boolean')
|
|
140
|
+
throw new ToolInputError({ message: 'includeToolEvents must be boolean.' });
|
|
141
|
+
return { chatId: input.chatId, limit, ...(fromPosition === undefined ? {} : { fromPosition }), includeToolEvents: input.includeToolEvents === true };
|
|
142
|
+
}
|
|
143
|
+
function projectMessage({ row, includeToolEvents, redact }, _options = {}) {
|
|
144
|
+
const content = redact({ text: row.content }).text;
|
|
145
|
+
const tools = includeToolEvents ? summarizeTools({ events: row.events, redact }) : undefined;
|
|
146
|
+
return capMessage({ view: { id: redact({ text: row.id }).text.slice(0, 128), role: row.role === 'user' ? 'user' : 'assistant', position: row.position,
|
|
147
|
+
content: content.slice(0, HISTORY_MESSAGE_CHAR_CAP), truncated: content.length > HISTORY_MESSAGE_CHAR_CAP || row.id.length > 128 || tools?.truncated === true,
|
|
148
|
+
...(tools ? { toolEvents: tools.items } : {}) } });
|
|
149
|
+
}
|
|
150
|
+
/** Fill the serialized page budget in candidate order; the first row always fits. */
|
|
151
|
+
function fillPage({ candidates, project }, _options = {}) {
|
|
152
|
+
const messages = [];
|
|
153
|
+
let truncated = false;
|
|
154
|
+
// Reserve result envelope/continuation space; measure serialized messages, including escapes.
|
|
155
|
+
let remaining = HISTORY_TOTAL_CHAR_CAP - 512;
|
|
156
|
+
for (const row of candidates) {
|
|
157
|
+
const view = project(row);
|
|
158
|
+
if (JSON.stringify(view).length > remaining && messages.length > 0) {
|
|
159
|
+
truncated = true;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
// Advance only over returned positions; the next page must include the omitted row.
|
|
163
|
+
messages.push(view);
|
|
164
|
+
remaining -= JSON.stringify(view).length + 1;
|
|
165
|
+
if (view.truncated)
|
|
166
|
+
truncated = true;
|
|
167
|
+
}
|
|
168
|
+
return { messages, truncated };
|
|
169
|
+
}
|
|
170
|
+
async function readChat({ ports, redact, ctx }, _options = {}) {
|
|
171
|
+
const { chatId, limit, fromPosition, includeToolEvents } = readInput({ input: ctx.input });
|
|
172
|
+
const { store, history } = await openHistory({ ports, principalId: ctx.principal.id });
|
|
173
|
+
if (!await store.get({ id: chatId }, {}))
|
|
174
|
+
throw new ToolInputError({ message: 'Chat is unavailable to this caller. Choose an id from assistant_chats_list.' });
|
|
175
|
+
const tail = fromPosition === undefined;
|
|
176
|
+
const page = await history.read({ chatId }, { ...(tail ? {} : { fromPosition }), limit, includeToolEvents });
|
|
177
|
+
const filled = fillPage({ candidates: tail ? [...page.messages].reverse() : page.messages, project: row => projectMessage({ row, includeToolEvents, redact }) });
|
|
178
|
+
return pageResult({ chatId: redact({ text: chatId }).text, tail, page, filled });
|
|
179
|
+
}
|
|
180
|
+
/** Continuation envelope: positions come only from returned rows, so no row is ever skipped. */
|
|
181
|
+
function pageResult({ chatId, tail, page, filled }, _options = {}) {
|
|
182
|
+
const messages = tail ? filled.messages.reverse() : filled.messages;
|
|
183
|
+
const last = messages.at(-1), first = messages.at(0);
|
|
184
|
+
const more = messages.length < page.messages.length || page.hasMore;
|
|
185
|
+
return { chatId, messages, truncated: page.hasMore || filled.truncated, hasMore: !tail && more,
|
|
186
|
+
...(last ? { nextPosition: last.position + 1 } : {}),
|
|
187
|
+
...(first ? { firstPosition: first.position } : {}),
|
|
188
|
+
hasMoreBefore: tail && more };
|
|
189
|
+
}
|
|
85
190
|
/** Build both history registrations, using the existing catalog/risk/metadata contract.
|
|
86
191
|
* @param ports Authenticated scoped-store port; denied/missing history must fail closed.
|
|
87
192
|
* @param options Optional stricter redaction policy (the secret-card policy is the default).
|
|
@@ -92,77 +197,8 @@ export function buildAssistantHistoryRegistrations({ ports }, { redact = redactU
|
|
|
92
197
|
return buildDomainRegistrations({ domain: 'assistant-history', catalogModule: 'chat/store/runtime/history-tools', catalog: assistantHistoryToolCatalog,
|
|
93
198
|
derivedRisk: assistantHistoryDerivedRisk, metadata: assistantHistoryToolMetadata,
|
|
94
199
|
handlers: {
|
|
95
|
-
assistant_chats_list:
|
|
96
|
-
|
|
97
|
-
if (input.query !== undefined && (typeof input.query !== 'string' || input.query.length > 200))
|
|
98
|
-
throw new ToolInputError({ message: 'query must be a string of at most 200 characters.' });
|
|
99
|
-
const limit = integerOption({ input, key: 'limit', fallback: 20, max: 50 });
|
|
100
|
-
const store = await ports.getStore({ principalId: ctx.principal.id }, {});
|
|
101
|
-
if (!store.history)
|
|
102
|
-
throw new ToolInputError({ message: 'Chat history is unavailable on this site.' });
|
|
103
|
-
const rows = await store.history.list({}, { ...(input.query === undefined ? {} : { query: input.query }), limit });
|
|
104
|
-
let truncated = false;
|
|
105
|
-
const chats = rows.map(row => {
|
|
106
|
-
// Redact BEFORE slicing: a cap must not turn a credential into an undetectable fragment.
|
|
107
|
-
const title = redact({ text: row.title ?? '' }).text;
|
|
108
|
-
const first = redact({ text: row.firstUserLine }).text.split(/\r?\n/, 1)[0];
|
|
109
|
-
if (title.length > 300 || first.length > 500 || row.id.length > 128)
|
|
110
|
-
truncated = true;
|
|
111
|
-
return { id: redact({ text: row.id }).text.slice(0, 128), title: title.slice(0, 300), updatedAt: row.updatedAt,
|
|
112
|
-
messageCount: row.messageCount, firstUserLine: first.slice(0, 500) };
|
|
113
|
-
});
|
|
114
|
-
// List is bounded too; serialized escaping can make text larger than its character count.
|
|
115
|
-
while (JSON.stringify({ chats, truncated }).length > HISTORY_TOTAL_CHAR_CAP) {
|
|
116
|
-
chats.pop();
|
|
117
|
-
truncated = true;
|
|
118
|
-
}
|
|
119
|
-
return { chats, truncated };
|
|
120
|
-
},
|
|
121
|
-
assistant_chat_read: async (ctx) => {
|
|
122
|
-
const input = inputRecord({ input: ctx.input, keys: ['chatId', 'fromPosition', 'limit', 'includeToolEvents'] });
|
|
123
|
-
if (typeof input.chatId !== 'string' || !input.chatId.trim() || input.chatId.length > 128)
|
|
124
|
-
throw new ToolInputError({ message: 'chatId must be an id returned by assistant_chats_list.' });
|
|
125
|
-
const limit = integerOption({ input, key: 'limit', fallback: 50, max: 100 });
|
|
126
|
-
const fromPosition = input.fromPosition === undefined ? undefined : integerOption({ input, key: 'fromPosition', fallback: 0, max: Number.MAX_SAFE_INTEGER, min: 0 });
|
|
127
|
-
if (input.includeToolEvents !== undefined && typeof input.includeToolEvents !== 'boolean')
|
|
128
|
-
throw new ToolInputError({ message: 'includeToolEvents must be boolean.' });
|
|
129
|
-
const store = await ports.getStore({ principalId: ctx.principal.id }, {});
|
|
130
|
-
if (!store.history)
|
|
131
|
-
throw new ToolInputError({ message: 'Chat history is unavailable on this site.' });
|
|
132
|
-
if (!await store.get({ id: input.chatId }, {}))
|
|
133
|
-
throw new ToolInputError({ message: 'Chat is unavailable to this caller. Choose an id from assistant_chats_list.' });
|
|
134
|
-
const page = await store.history.read({ chatId: input.chatId }, { ...(fromPosition === undefined ? {} : { fromPosition }), limit, includeToolEvents: input.includeToolEvents === true });
|
|
135
|
-
const messages = [];
|
|
136
|
-
let truncated = page.hasMore;
|
|
137
|
-
// Reserve result envelope/continuation space; measure serialized messages, including escapes.
|
|
138
|
-
let remaining = HISTORY_TOTAL_CHAR_CAP - 512;
|
|
139
|
-
const candidates = fromPosition === undefined ? [...page.messages].reverse() : page.messages;
|
|
140
|
-
for (const row of candidates) {
|
|
141
|
-
const content = redact({ text: row.content }).text;
|
|
142
|
-
const tools = input.includeToolEvents === true ? summarizeTools({ events: row.events, redact }) : undefined;
|
|
143
|
-
const view = capMessage({ view: { id: redact({ text: row.id }).text.slice(0, 128), role: row.role === 'user' ? 'user' : 'assistant', position: row.position,
|
|
144
|
-
content: content.slice(0, HISTORY_MESSAGE_CHAR_CAP), truncated: content.length > HISTORY_MESSAGE_CHAR_CAP || row.id.length > 128 || tools?.truncated === true,
|
|
145
|
-
...(tools ? { toolEvents: tools.items } : {}) } });
|
|
146
|
-
if (JSON.stringify(view).length > remaining && messages.length > 0) {
|
|
147
|
-
truncated = true;
|
|
148
|
-
break;
|
|
149
|
-
}
|
|
150
|
-
// Advance only over returned positions; the next page must include the omitted row.
|
|
151
|
-
messages.push(view);
|
|
152
|
-
remaining -= JSON.stringify(view).length + 1;
|
|
153
|
-
if (view.truncated)
|
|
154
|
-
truncated = true;
|
|
155
|
-
}
|
|
156
|
-
if (fromPosition === undefined)
|
|
157
|
-
messages.reverse();
|
|
158
|
-
const last = messages.at(-1), first = messages.at(0);
|
|
159
|
-
const omitted = messages.length < page.messages.length;
|
|
160
|
-
const hasMore = fromPosition !== undefined && (omitted || page.hasMore);
|
|
161
|
-
return { chatId: redact({ text: input.chatId }).text, messages, truncated, hasMore,
|
|
162
|
-
...(last ? { nextPosition: last.position + 1 } : {}),
|
|
163
|
-
...(first ? { firstPosition: first.position } : {}),
|
|
164
|
-
hasMoreBefore: fromPosition === undefined && (omitted || page.hasMore) };
|
|
165
|
-
},
|
|
200
|
+
assistant_chats_list: ctx => listChats({ ports, redact, ctx }),
|
|
201
|
+
assistant_chat_read: ctx => readChat({ ports, redact, ctx }),
|
|
166
202
|
},
|
|
167
203
|
});
|
|
168
204
|
}
|
|
@@ -5,11 +5,14 @@ import type { ChatRunLedger } from "../../core/durable-runs/ledger-contracts.js"
|
|
|
5
5
|
export { runLockKey } from "../../core/durable-runs/run-lock.js";
|
|
6
6
|
/**
|
|
7
7
|
* @param store the chat kernel, or the open `chat.db` handle `createChatStoreFactory` also wraps.
|
|
8
|
+
* @param now Injectable epoch-millisecond clock for progress stamps and the default cancel time.
|
|
8
9
|
* @complexity each method is one or two indexed statements, except `reconcileInterrupted`, which
|
|
9
10
|
* is O(stuck rows).
|
|
10
11
|
*/
|
|
11
12
|
export declare function createChatRunLedger<DB extends ChatDatabase>({ kernel: hostKernel, isDaemonRunId }: {
|
|
12
13
|
kernel: ChatKernel<DB>;
|
|
13
14
|
isDaemonRunId: RunIdPolicy;
|
|
14
|
-
},
|
|
15
|
+
}, { now }?: {
|
|
16
|
+
now?: () => number;
|
|
17
|
+
}): ChatRunLedger;
|
|
15
18
|
//# sourceMappingURL=run-ledger.d.ts.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dedupeToolUsesById } from "../../core/tool-events.js";
|
|
1
2
|
import { chatKernelTables } from "../sql/boundary.js";
|
|
2
3
|
import { createDurableRunStore } from "./durable-run-store.js";
|
|
3
4
|
/**
|
|
@@ -47,27 +48,64 @@ function readEvents(eventsJson) {
|
|
|
47
48
|
}
|
|
48
49
|
return Array.isArray(events) ? events : [];
|
|
49
50
|
}
|
|
51
|
+
/** Tool identity shared by progress detection and checkpoint merging; "" for non-tool events. */
|
|
52
|
+
function toolKey(event) {
|
|
53
|
+
if (event.kind === "tool_use")
|
|
54
|
+
return `use:${event.id}`;
|
|
55
|
+
if (event.kind === "tool_result")
|
|
56
|
+
return `result:${event.toolUseId}`;
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
50
59
|
function hasNewToolProgress(current, next) {
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
return next.some((event) => { const id = key(event); return id !== "" && !saved.has(id); });
|
|
60
|
+
const saved = new Set(current.map(toolKey));
|
|
61
|
+
return next.some((event) => { const id = toolKey(event); return id !== "" && !saved.has(id); });
|
|
54
62
|
}
|
|
63
|
+
/** Retain a longer checkpoint's text and merge non-text replay events under the shared ID rule.
|
|
64
|
+
* @complexity O(b + n) time and space for b serialized bytes across n events. */
|
|
55
65
|
function retainSavedEvents(saved, incoming) {
|
|
56
66
|
const keys = new Set(saved.map((event) => JSON.stringify(event)));
|
|
57
67
|
const additions = incoming.filter((event) => event.kind !== "text" && !keys.has(JSON.stringify(event)));
|
|
58
|
-
return [...saved, ...additions];
|
|
68
|
+
return dedupeToolUsesById({ events: [...saved, ...additions] });
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The row a checkpoint writes, whether it is progress, and whether it changes anything.
|
|
72
|
+
*
|
|
73
|
+
* The saved `events_json` is parsed ONCE and reused by both the merge and the progress check:
|
|
74
|
+
* a long turn's column is many MB, and a host checkpoints every second (memory cause 3).
|
|
75
|
+
* Appending only new events instead of rewriting the column is NOT equivalent: storage coalesces
|
|
76
|
+
* text deltas (the last text event grows in place) and the merge puts saved-only tool barriers
|
|
77
|
+
* after the replay's events, so an existing element or the tail changes on most checkpoints.
|
|
78
|
+
* @complexity O(b + n) time and space for b serialized bytes across n events; one parse, one stringify.
|
|
79
|
+
*/
|
|
80
|
+
function planCheckpoint({ current, progress }, _optional = {}) {
|
|
81
|
+
const saved = readEvents(current.events_json);
|
|
82
|
+
const events = keepToolCheckpoints(saved, progress.events);
|
|
83
|
+
const eventsJson = JSON.stringify(events);
|
|
84
|
+
// A checkpoint is the run producing output, so an accepted (`queued`) row is now `running`;
|
|
85
|
+
// nothing else moves it there, and a streaming answer must not read as still waiting.
|
|
86
|
+
const runStatus = current.run_status === "queued" ? "running" : current.run_status;
|
|
87
|
+
return {
|
|
88
|
+
eventsJson,
|
|
89
|
+
runStatus,
|
|
90
|
+
grown: progress.content.length > current.content.length || hasNewToolProgress(saved, events),
|
|
91
|
+
unchanged: runStatus === current.run_status && progress.content === current.content && eventsJson === current.events_json,
|
|
92
|
+
};
|
|
59
93
|
}
|
|
94
|
+
/** Preserve tool barriers absent from a replay, with one persisted use per tool ID.
|
|
95
|
+
* @complexity O(n) time and space in the combined saved and incoming event count. */
|
|
60
96
|
function keepToolCheckpoints(saved, incoming) {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
97
|
+
const seen = new Set(incoming.map(toolKey));
|
|
98
|
+
// Deduplicate after merging: a legacy checkpoint or replay can repeat an ID with different
|
|
99
|
+
// serialized input. The shared ID rule owns identity; JSON equality does not.
|
|
100
|
+
return dedupeToolUsesById({ events: [...incoming, ...saved.filter((event) => { const id = toolKey(event); return id !== "" && !seen.has(id); })] });
|
|
64
101
|
}
|
|
65
102
|
/**
|
|
66
103
|
* @param store the chat kernel, or the open `chat.db` handle `createChatStoreFactory` also wraps.
|
|
104
|
+
* @param now Injectable epoch-millisecond clock for progress stamps and the default cancel time.
|
|
67
105
|
* @complexity each method is one or two indexed statements, except `reconcileInterrupted`, which
|
|
68
106
|
* is O(stuck rows).
|
|
69
107
|
*/
|
|
70
|
-
export function createChatRunLedger({ kernel: hostKernel, isDaemonRunId },
|
|
108
|
+
export function createChatRunLedger({ kernel: hostKernel, isDaemonRunId }, { now = () => Date.now() } = {}) {
|
|
71
109
|
const kernel = chatKernelTables({ kernel: hostKernel }, {});
|
|
72
110
|
async function isSettled(run) {
|
|
73
111
|
const row = await kernel.run((db) => db
|
|
@@ -130,21 +168,23 @@ export function createChatRunLedger({ kernel: hostKernel, isDaemonRunId }, _opti
|
|
|
130
168
|
.where((eb) => eb.and([isRunRow(eb, progress), notTerminal(eb)])).executeTakeFirst());
|
|
131
169
|
if (!current || progress.content.length < current.content.length)
|
|
132
170
|
return false;
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
171
|
+
const { eventsJson, runStatus, grown, unchanged } = planCheckpoint({ current, progress });
|
|
172
|
+
// An identical checkpoint would rewrite the same multi-MB column for nothing. Under this lock
|
|
173
|
+
// the row was just matched by the update's own predicate, so the update would match (true),
|
|
174
|
+
// and identical content and events are never progress (`grown` is false), so skipping it
|
|
175
|
+
// leaves the row and the recovery budget exactly as the write would have.
|
|
176
|
+
if (unchanged)
|
|
177
|
+
return true;
|
|
138
178
|
const result = await kernel.run((db) => db
|
|
139
179
|
.updateTable("ai_chat_messages")
|
|
140
|
-
.set({ content: progress.content, events_json:
|
|
180
|
+
.set({ content: progress.content, events_json: eventsJson, run_status: runStatus })
|
|
141
181
|
.where((eb) => eb.and([isRunRow(eb, progress), notTerminal(eb)]))
|
|
142
182
|
.executeTakeFirst());
|
|
143
183
|
if (Number(result.numUpdatedRows) > 0 && grown) {
|
|
144
184
|
// Progress earns a fresh recovery budget. Replayed prefixes and the coordinator's
|
|
145
185
|
// divider are not progress and cannot keep a repeatedly failing attempt alive forever.
|
|
146
186
|
await kernel.run((db) => db.updateTable("assistant_run_attempts")
|
|
147
|
-
.set({ recovery_count: 0, recovery_deadline: null, recovery_elapsed_ms: 0, last_progress_at:
|
|
187
|
+
.set({ recovery_count: 0, recovery_deadline: null, recovery_elapsed_ms: 0, last_progress_at: now() })
|
|
148
188
|
.where("message_id", "=", progress.messageId).execute());
|
|
149
189
|
}
|
|
150
190
|
return Number(result.numUpdatedRows) > 0;
|
|
@@ -188,7 +228,7 @@ export function createChatRunLedger({ kernel: hostKernel, isDaemonRunId }, _opti
|
|
|
188
228
|
.where(matches).executeTakeFirst());
|
|
189
229
|
if (!current)
|
|
190
230
|
return false;
|
|
191
|
-
const endedAt = required.now ??
|
|
231
|
+
const endedAt = required.now ?? now();
|
|
192
232
|
const events = [...readEvents(current.events_json), { kind: "status", label: "Stopped. Saved work is above." }];
|
|
193
233
|
const result = await kernel.run((db) => db.updateTable("ai_chat_messages")
|
|
194
234
|
.set({ run_status: "canceled", ended_at: endedAt, events_json: JSON.stringify(events) })
|
package/dist/store/sql/cursor.js
CHANGED
|
@@ -16,34 +16,54 @@ export function encodeCursor({ kind, scope, conversation, anchor }) {
|
|
|
16
16
|
const binary = Array.from(bytes, byte => String.fromCharCode(byte)).join('');
|
|
17
17
|
return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
|
|
18
18
|
}
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
/** Every structural failure is the same refusal; callers never learn which check rejected it. */
|
|
20
|
+
function invalidCursor() {
|
|
21
|
+
throw new ChatStoreError({ code: 'invalid-cursor' });
|
|
22
|
+
}
|
|
23
|
+
/** Shape check before decoding: a non-empty, bounded base64url string. Carries no cause. */
|
|
24
|
+
function assertCursorText({ cursor }) {
|
|
25
|
+
if (typeof cursor !== 'string' || cursor.length === 0 || cursor.length > 8192 || !/^[A-Za-z0-9_-]+$/.test(cursor))
|
|
26
|
+
invalidCursor();
|
|
27
|
+
}
|
|
28
|
+
/** base64url -> strict UTF-8 -> JSON; any decode failure is wrapped with its cause attached. */
|
|
29
|
+
function decodeBody({ cursor }) {
|
|
27
30
|
try {
|
|
28
31
|
const binary = atob(cursor.replaceAll('-', '+').replaceAll('_', '/'));
|
|
29
32
|
const bytes = Uint8Array.from(binary, char => char.charCodeAt(0));
|
|
30
|
-
|
|
33
|
+
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
31
34
|
}
|
|
32
35
|
catch (cause) {
|
|
33
36
|
throw new ChatStoreError({ code: 'invalid-cursor' }, { cause });
|
|
34
37
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
}
|
|
39
|
+
/** Version, exact length, read kind and conversation binding. */
|
|
40
|
+
function readEnvelope({ value, kind, conversation }) {
|
|
41
|
+
if (!Array.isArray(value) || value.length !== 5 || value[0] !== 1 || value[1] !== kind || value[3] !== conversation)
|
|
42
|
+
invalidCursor();
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
/** The cursor must carry exactly this scope's owner tuple. */
|
|
46
|
+
function assertOwner({ owner, scope }) {
|
|
47
|
+
const expected = ownerTuple(scope);
|
|
48
|
+
if (!Array.isArray(owner) || owner.length !== 3 || owner.some((part, i) => part !== expected[i]))
|
|
49
|
+
invalidCursor();
|
|
50
|
+
}
|
|
51
|
+
/** A [finite number, string] pair; message positions must also be non-negative safe integers. */
|
|
52
|
+
function readAnchor({ anchor, kind }) {
|
|
42
53
|
if (!Array.isArray(anchor) || anchor.length !== 2 || typeof anchor[0] !== 'number' || !Number.isFinite(anchor[0]) || typeof anchor[1] !== 'string') {
|
|
43
|
-
|
|
54
|
+
invalidCursor();
|
|
44
55
|
}
|
|
45
56
|
if (kind === 'messages' && (!Number.isSafeInteger(anchor[0]) || anchor[0] < 0))
|
|
46
|
-
|
|
57
|
+
invalidCursor();
|
|
47
58
|
return [anchor[0], anchor[1]];
|
|
48
59
|
}
|
|
60
|
+
/** Validate version, exact structure, scope and read bindings before a query can run. */
|
|
61
|
+
export function decodeCursor({ cursor, kind, scope, conversation }) {
|
|
62
|
+
if (cursor === undefined)
|
|
63
|
+
return undefined;
|
|
64
|
+
assertCursorText({ cursor });
|
|
65
|
+
const envelope = readEnvelope({ value: decodeBody({ cursor }), kind, conversation });
|
|
66
|
+
assertOwner({ owner: envelope[2], scope });
|
|
67
|
+
return readAnchor({ anchor: envelope[4], kind });
|
|
68
|
+
}
|
|
49
69
|
//# sourceMappingURL=cursor.js.map
|
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import { type Clock } from '@jini-ai/core/primitives';
|
|
2
2
|
import { type StorageKernel } from '@jini-ai/db/kernel';
|
|
3
3
|
import { type UserTextRedactionOptions } from '../../core/user-text-redaction.js';
|
|
4
|
+
import type { ChatMessage } from '../../core/messages.js';
|
|
4
5
|
import type { ChatStore, ChatOwnerScope } from '../ports.js';
|
|
5
6
|
import type { ChatDatabase } from './tables.js';
|
|
7
|
+
interface MessageRow {
|
|
8
|
+
id: string;
|
|
9
|
+
role: string;
|
|
10
|
+
content: string;
|
|
11
|
+
agent_id: string | null;
|
|
12
|
+
agent_name: string | null;
|
|
13
|
+
events_json: string | null;
|
|
14
|
+
attachments_json: string | null;
|
|
15
|
+
run_id: string | null;
|
|
16
|
+
run_status: string | null;
|
|
17
|
+
created_at: number;
|
|
18
|
+
started_at: number | null;
|
|
19
|
+
ended_at: number | null;
|
|
20
|
+
}
|
|
21
|
+
/** Project only existing ai_* fields: resumable and lastRunEventId belong to run state. */
|
|
22
|
+
export declare function toMessage(row: MessageRow): ChatMessage;
|
|
6
23
|
/**
|
|
7
24
|
* Returns a {@link ChatHistoryStore} that can only ever see `scope`'s own conversations.
|
|
8
25
|
*
|
|
@@ -20,4 +37,5 @@ export declare function createSqlChatStore<DB extends ChatDatabase>({ kernel, sc
|
|
|
20
37
|
}, { clock, ...redactionOptions }?: {
|
|
21
38
|
clock?: Clock;
|
|
22
39
|
} & UserTextRedactionOptions): ChatStore;
|
|
40
|
+
export {};
|
|
23
41
|
//# sourceMappingURL=store.d.ts.map
|
package/dist/store/sql/store.js
CHANGED
|
@@ -27,9 +27,10 @@ function conversationLockKey(conversationId) {
|
|
|
27
27
|
function toConversation(row) {
|
|
28
28
|
return {
|
|
29
29
|
id: row.id,
|
|
30
|
-
title: row.title
|
|
31
|
-
titleSource:
|
|
32
|
-
|
|
30
|
+
title: row.title,
|
|
31
|
+
titleSource: row.title_source,
|
|
32
|
+
// COUNT(*) never yields null; Number() also maps a driver's string/bigint count.
|
|
33
|
+
messageCount: Number(row.message_count),
|
|
33
34
|
createdAt: Number(row.created_at),
|
|
34
35
|
updatedAt: Number(row.updated_at),
|
|
35
36
|
...(row.expires_at === null ? {} : { expiresAt: Number(row.expires_at) }),
|
|
@@ -47,6 +48,10 @@ function safeParse(value) {
|
|
|
47
48
|
return undefined;
|
|
48
49
|
}
|
|
49
50
|
}
|
|
51
|
+
/** A NULL (or empty) JSON column is absent; anything else goes through {@link safeParse}. */
|
|
52
|
+
function parseJsonColumn(value) {
|
|
53
|
+
return value ? safeParse(value) : undefined;
|
|
54
|
+
}
|
|
50
55
|
/** Project only existing ai_* fields: resumable and lastRunEventId belong to run state. */
|
|
51
56
|
// Both fields disappear on a transcript round trip today. Adding columns alone would let the
|
|
52
57
|
// browser persist claims about a run it does not own. First associate the run with its conversation
|
|
@@ -54,22 +59,52 @@ function safeParse(value) {
|
|
|
54
59
|
// The legacy messages store does retain last_run_event_id; that is a separate trust model, not
|
|
55
60
|
// evidence that this projection preserves it. A schema extension also needs host-owned follow-up
|
|
56
61
|
// migrations, because changing the DDL constant cannot update databases already migrated.
|
|
57
|
-
function toMessage(row) {
|
|
58
|
-
const events = row.events_json ? safeParse(row.events_json) : undefined;
|
|
59
|
-
const attachments = row.attachments_json ? safeParse(row.attachments_json) : undefined;
|
|
62
|
+
export function toMessage(row) {
|
|
60
63
|
return {
|
|
61
64
|
id: row.id, role: row.role, content: row.content,
|
|
65
|
+
...agentFields(row),
|
|
66
|
+
...jsonFields(row),
|
|
67
|
+
...runFields(row),
|
|
68
|
+
createdAt: Number(row.created_at),
|
|
69
|
+
...(row.started_at === null ? {} : { startedAt: Number(row.started_at) }),
|
|
70
|
+
...(row.ended_at === null ? {} : { endedAt: Number(row.ended_at) }),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** Empty agent columns are omitted, not projected as empty strings. */
|
|
74
|
+
function agentFields(row) {
|
|
75
|
+
return {
|
|
62
76
|
...(row.agent_id ? { agentId: row.agent_id } : {}),
|
|
63
77
|
...(row.agent_name ? { agentName: row.agent_name } : {}),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** Unparseable or absent JSON columns are omitted rather than failing the read. */
|
|
81
|
+
function jsonFields(row) {
|
|
82
|
+
const events = parseJsonColumn(row.events_json);
|
|
83
|
+
const attachments = parseJsonColumn(row.attachments_json);
|
|
84
|
+
return {
|
|
64
85
|
...(events === undefined ? {} : { events }),
|
|
65
86
|
...(attachments === undefined ? {} : { attachments }),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** Empty run columns are omitted. */
|
|
90
|
+
function runFields(row) {
|
|
91
|
+
return {
|
|
66
92
|
...(row.run_id ? { runId: row.run_id } : {}),
|
|
67
93
|
...(row.run_status ? { runStatus: row.run_status } : {}),
|
|
68
|
-
...(row.created_at === null ? {} : { createdAt: Number(row.created_at) }),
|
|
69
|
-
...(row.started_at === null ? {} : { startedAt: Number(row.started_at) }),
|
|
70
|
-
...(row.ended_at === null ? {} : { endedAt: Number(row.ended_at) }),
|
|
71
94
|
};
|
|
72
95
|
}
|
|
96
|
+
/** Reject an unbounded or malformed history read before any query runs. */
|
|
97
|
+
function assertHistoryReadBounds({ limit, fromPosition }) {
|
|
98
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100 || (fromPosition !== undefined && (!Number.isSafeInteger(fromPosition) || fromPosition < 0))) {
|
|
99
|
+
throw new ChatStoreError({ code: 'invalid-input' });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Project one history row; `events` is present (possibly undefined) only when tool events were asked for. */
|
|
103
|
+
function toHistoryMessage(row, { includeToolEvents }) {
|
|
104
|
+
return { id: row.id, role: row.role, content: row.content,
|
|
105
|
+
position: Number(row.position), createdAt: Number(row.created_at),
|
|
106
|
+
...(includeToolEvents ? { events: parseJsonColumn(row.events_json) } : {}), };
|
|
107
|
+
}
|
|
73
108
|
/**
|
|
74
109
|
* Returns a {@link ChatHistoryStore} that can only ever see `scope`'s own conversations.
|
|
75
110
|
*
|
|
@@ -240,9 +275,7 @@ export function createSqlChatStore({ kernel, scope: inputScope }, { clock = crea
|
|
|
240
275
|
},
|
|
241
276
|
/** No attachment column is read. O(limit) rows; default tail is reversed into position order. */
|
|
242
277
|
async read({ chatId }, { fromPosition, limit = 50, includeToolEvents = false } = {}) {
|
|
243
|
-
|
|
244
|
-
throw new ChatStoreError({ code: 'invalid-input' });
|
|
245
|
-
}
|
|
278
|
+
assertHistoryReadBounds({ limit, fromPosition });
|
|
246
279
|
const rows = await run(db => {
|
|
247
280
|
let statement = ownedMessages(db, chatId).clearSelect().select([
|
|
248
281
|
'm.id', 'm.role', 'm.content', 'm.position', 'm.created_at',
|
|
@@ -255,9 +288,7 @@ export function createSqlChatStore({ kernel, scope: inputScope }, { clock = crea
|
|
|
255
288
|
const page = rows.slice(0, limit);
|
|
256
289
|
if (fromPosition === undefined)
|
|
257
290
|
page.reverse();
|
|
258
|
-
return { messages: page.map(row => ({
|
|
259
|
-
position: Number(row.position), createdAt: Number(row.created_at),
|
|
260
|
-
...(includeToolEvents ? { events: row.events_json ? safeParse(row.events_json) : undefined } : {}), })), hasMore: rows.length > limit };
|
|
291
|
+
return { messages: page.map(row => toHistoryMessage(row, { includeToolEvents })), hasMore: rows.length > limit };
|
|
261
292
|
},
|
|
262
293
|
},
|
|
263
294
|
async list() {
|