@jini-ai/chat 0.5.7 → 0.5.8
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/core/ag-ui/entry.js +2 -0
- package/dist/core/assistant-client/durable-run-subscription.d.ts +4 -1
- package/dist/core/assistant-client/durable-run-subscription.js +13 -4
- package/dist/core/events.d.ts +3 -0
- package/dist/core/history-tool-metadata.d.ts +23 -0
- package/dist/core/history-tool-metadata.js +13 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +3 -0
- package/dist/core/persistence/title.d.ts +24 -0
- package/dist/core/persistence/title.js +60 -9
- package/dist/core/run-activity.d.ts +15 -2
- package/dist/core/run-activity.js +6 -4
- package/dist/core/run-events/entry.js +2 -0
- package/dist/core/tool-call-detail.d.ts +15 -0
- package/dist/core/tool-call-detail.js +18 -0
- package/dist/core/turn-usage.d.ts +42 -3
- package/dist/core/turn-usage.js +101 -4
- package/dist/core/usage-format.d.ts +10 -0
- package/dist/core/usage-format.js +15 -0
- package/dist/embed-widget/browser/lazy-loader.d.ts +34 -0
- package/dist/embed-widget/browser/lazy-loader.js +116 -0
- package/dist/embed-widget/react/dialog.hooks.d.ts +28 -0
- package/dist/embed-widget/react/dialog.hooks.js +54 -0
- package/dist/embed-widget/react/embed.d.ts +1 -0
- package/dist/embed-widget/react/embed.js +5 -3
- package/dist/embed-widget/react/header.d.ts +3 -1
- package/dist/embed-widget/react/header.hooks.d.ts +13 -0
- package/dist/embed-widget/react/header.hooks.js +32 -0
- package/dist/embed-widget/react/header.js +4 -14
- package/dist/embed-widget/vanilla/index.d.ts +63 -0
- package/dist/embed-widget/vanilla/index.js +319 -0
- package/dist/embed-widget/vanilla/state.d.ts +48 -0
- package/dist/embed-widget/vanilla/state.js +120 -0
- package/dist/embed-widget/vanilla/style.css +29 -0
- package/dist/react/components/A2uiSurfaceCard.d.ts +5 -1
- package/dist/react/components/A2uiSurfaceCard.js +9 -9
- package/dist/react/components/AttachmentPreviewModal.hooks.d.ts +7 -0
- package/dist/react/components/AttachmentPreviewModal.hooks.js +1 -1
- package/dist/react/components/ChatFab.d.ts +4 -1
- package/dist/react/components/ChatFab.js +2 -2
- package/dist/react/components/ConversationList.d.ts +8 -1
- package/dist/react/components/ConversationList.hooks.d.ts +18 -0
- package/dist/react/components/ConversationList.hooks.js +25 -0
- package/dist/react/components/ConversationList.js +12 -5
- package/dist/react/components/ConversationList.rules.d.ts +25 -0
- package/dist/react/components/ConversationList.rules.js +19 -0
- package/dist/react/components/McpUiSurfaceCard.d.ts +1 -1
- package/dist/react/components/McpUiSurfaceCard.js +8 -4
- package/dist/react/components/MessageList.d.ts +1 -1
- package/dist/react/components/MessageList.hooks.d.ts +4 -0
- package/dist/react/components/MessageList.hooks.js +6 -1
- package/dist/react/components/MessageList.js +4 -3
- package/dist/react/components/MessageRow.d.ts +17 -0
- package/dist/react/components/MessageRow.js +54 -35
- package/dist/react/components/MessageRow.usage.d.ts +48 -0
- package/dist/react/components/MessageRow.usage.js +47 -0
- package/dist/react/components/MidRunUserMessage.d.ts +2 -12
- package/dist/react/components/MidRunUserMessage.js +4 -2
- package/dist/react/components/SentAttachment.d.ts +19 -0
- package/dist/react/components/SentAttachment.hooks.d.ts +44 -0
- package/dist/react/components/SentAttachment.hooks.js +56 -0
- package/dist/react/components/SentAttachment.js +14 -0
- package/dist/react/components/ToolCallGroup.d.ts +12 -0
- package/dist/react/components/ToolCallGroup.hooks.d.ts +20 -0
- package/dist/react/components/ToolCallGroup.hooks.js +23 -0
- package/dist/react/components/ToolCallGroup.js +13 -0
- package/dist/react/components/ToolCard.js +25 -17
- package/dist/react/components/mcp-ui-surface-state.d.ts +21 -0
- package/dist/react/components/mcp-ui-surface-state.js +26 -0
- package/dist/react/ext-event-renderer-registry.d.ts +7 -0
- package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
- package/dist/react/features/chat-pane/components/ChatPane.js +11 -6
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +1 -0
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +16 -10
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +4 -0
- package/dist/react/features/chat-pane/hooks/useChatPaneSuggestion.hooks.d.ts +9 -0
- package/dist/react/features/chat-pane/hooks/useChatPaneSuggestion.hooks.js +11 -0
- package/dist/react/features/chat-pane/hooks/useChatPaneSurface.hooks.d.ts +17 -0
- package/dist/react/features/chat-pane/hooks/useChatPaneSurface.hooks.js +15 -0
- package/dist/react/features/chat-pane/rules.d.ts +14 -0
- package/dist/react/features/chat-pane/rules.js +18 -0
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +85 -1
- package/dist/react/features/chat-pane/types.d.ts +16 -1
- package/dist/react/hooks/useAnsweredCards.d.ts +20 -0
- package/dist/react/hooks/useAnsweredCards.js +23 -0
- package/dist/react/hooks/useConversation.d.ts +8 -0
- package/dist/react/hooks/useConversation.js +34 -0
- package/dist/react/hooks/useRunActivity.d.ts +5 -3
- package/dist/react/hooks/useRunActivity.js +4 -3
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.js +3 -0
- package/dist/react/message-blocks.d.ts +22 -3
- package/dist/react/message-blocks.js +25 -3
- package/dist/react/tool-call-display.d.ts +90 -0
- package/dist/react/tool-call-display.js +97 -0
- package/dist/store/ports.d.ts +25 -2
- package/dist/store/runtime/entry.d.ts +1 -0
- package/dist/store/runtime/entry.js +1 -0
- package/dist/store/runtime/history-tools.d.ts +27 -0
- package/dist/store/runtime/history-tools.js +169 -0
- package/dist/store/sql/store.js +52 -0
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.js +22 -0
- package/package.json +48 -18
package/dist/store/ports.d.ts
CHANGED
|
@@ -63,6 +63,8 @@ export interface ChatSearchCapability {
|
|
|
63
63
|
* @example store.rename({ id, title }, { source: 'generated' }); store.pageConversations({}, { limit: 50 })
|
|
64
64
|
*/
|
|
65
65
|
export interface ChatStore {
|
|
66
|
+
/** Bounded history discovery/read capability; always uses this store's immutable owner scope. */
|
|
67
|
+
readonly history?: ChatHistoryReader;
|
|
66
68
|
/** Compatibility read: all owned conversations, updatedAt descending. */
|
|
67
69
|
list(): Promise<ChatConversation[]>;
|
|
68
70
|
/** Unknown and other-owned IDs both return null. */
|
|
@@ -97,8 +99,29 @@ export interface ChatStore {
|
|
|
97
99
|
pageMessages(input: ChatMessagePageInput, options?: ChatPageOptions): Promise<ChatPage<ChatMessage>>;
|
|
98
100
|
readonly search?: ChatSearchCapability;
|
|
99
101
|
}
|
|
100
|
-
/** Eight-method subset
|
|
101
|
-
export type ChatHistoryStore = Pick<ChatStore, 'list' | 'get' | 'create' | 'rename' | 'touch' | 'delete' | 'messages' | 'appendMessage'>;
|
|
102
|
+
/** Eight-method compatibility subset plus optional bounded history; old hosts/fakes still fit. */
|
|
103
|
+
export type ChatHistoryStore = Pick<ChatStore, 'list' | 'get' | 'create' | 'rename' | 'touch' | 'delete' | 'messages' | 'appendMessage' | 'history'>;
|
|
104
|
+
/** Internal persistence projections. Model-facing tools redact and cap these before returning. */
|
|
105
|
+
export interface ChatHistoryReader {
|
|
106
|
+
list(required: Record<string, never>, optional?: {
|
|
107
|
+
query?: string;
|
|
108
|
+
limit?: number;
|
|
109
|
+
}): Promise<readonly (ChatConversation & {
|
|
110
|
+
firstUserLine: string;
|
|
111
|
+
})[]>;
|
|
112
|
+
read(required: {
|
|
113
|
+
chatId: string;
|
|
114
|
+
}, optional?: {
|
|
115
|
+
fromPosition?: number;
|
|
116
|
+
limit?: number;
|
|
117
|
+
includeToolEvents?: boolean;
|
|
118
|
+
}): Promise<{
|
|
119
|
+
messages: readonly (ChatMessage & {
|
|
120
|
+
position: number;
|
|
121
|
+
})[];
|
|
122
|
+
hasMore: boolean;
|
|
123
|
+
}>;
|
|
124
|
+
}
|
|
102
125
|
/** Authority for cross-owner retention must never reach ordinary request handlers. */
|
|
103
126
|
export interface ChatHistoryMaintenance {
|
|
104
127
|
/** Delete at most limit conversations; default 500, integer 1..500. */
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Model-facing history tools over an authenticated owner-scoped store; no database handles. */
|
|
2
|
+
import { type AgentToolDefinition, type DerivedRiskByToolId, type ToolRegistration } from '@jini-ai/core';
|
|
3
|
+
import { type RedactUserText } from '../../core/user-text-redaction.js';
|
|
4
|
+
import type { ChatHistoryStore } from '../ports.js';
|
|
5
|
+
export declare const HISTORY_MESSAGE_CHAR_CAP = 8000;
|
|
6
|
+
export declare const HISTORY_TOTAL_CHAR_CAP = 40000;
|
|
7
|
+
export declare const assistantHistoryDerivedRisk: DerivedRiskByToolId;
|
|
8
|
+
/** Exported so host contract tests can pin each registration to its catalog entry. */
|
|
9
|
+
export declare const assistantHistoryToolCatalog: ReadonlyMap<string, AgentToolDefinition>;
|
|
10
|
+
/** Auth is resolved by the host using execution identity, never from model input. */
|
|
11
|
+
export interface AssistantHistoryToolPorts {
|
|
12
|
+
getStore(required: {
|
|
13
|
+
principalId: string;
|
|
14
|
+
}, optional?: Record<string, never>): Promise<ChatHistoryStore>;
|
|
15
|
+
}
|
|
16
|
+
/** Build both history registrations, using the existing catalog/risk/metadata contract.
|
|
17
|
+
* @param ports Authenticated scoped-store port; denied/missing history must fail closed.
|
|
18
|
+
* @param options Optional stricter redaction policy (the secret-card policy is the default).
|
|
19
|
+
* @returns Read-only tool registrations. @throws ToolInputError for malformed or refused reads.
|
|
20
|
+
* @complexity Bounded projections; persistence owns search cost and message ordering.
|
|
21
|
+
*/
|
|
22
|
+
export declare function buildAssistantHistoryRegistrations({ ports }: {
|
|
23
|
+
ports: AssistantHistoryToolPorts;
|
|
24
|
+
}, { redact }?: {
|
|
25
|
+
redact?: RedactUserText;
|
|
26
|
+
}): ToolRegistration[];
|
|
27
|
+
//# sourceMappingURL=history-tools.d.ts.map
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/** Model-facing history tools over an authenticated owner-scoped store; no database handles. */
|
|
2
|
+
import { buildDomainRegistrations, indexCatalogById, requireInputRecord, ToolInputError } from '@jini-ai/core';
|
|
3
|
+
import { redactUserText } from '../../core/user-text-redaction.js';
|
|
4
|
+
import { assistantHistoryToolMetadata } from '../../core/history-tool-metadata.js';
|
|
5
|
+
export const HISTORY_MESSAGE_CHAR_CAP = 8_000;
|
|
6
|
+
export const HISTORY_TOTAL_CHAR_CAP = 40_000;
|
|
7
|
+
const TOOL_EVENT_CAP = 50;
|
|
8
|
+
export const assistantHistoryDerivedRisk = new Map([
|
|
9
|
+
['assistant_chats_list', 'none'], ['assistant_chat_read', 'none'],
|
|
10
|
+
]);
|
|
11
|
+
/** Exported so host contract tests can pin each registration to its catalog entry. */
|
|
12
|
+
export const assistantHistoryToolCatalog = indexCatalogById({ catalog: [
|
|
13
|
+
{ name: 'assistant_chats_list', sideEffects: 'none', authorization: { permission: 'admin.assistant.use' },
|
|
14
|
+
description: "Lists the CURRENT user's own conversations on this site, newest first, optionally searching titles and message content. Returns id, title, updatedAt, messageCount and firstUserLine, with redacted, capped text. When the user refers to an earlier or interrupted chat ('continue what we were doing'), list first, then assistant_chat_read to read its tail, summarise the state and continue. No other user's chats are accessible; unavailable history is an error.",
|
|
15
|
+
inputSchema: { type: 'object', additionalProperties: false, properties: {
|
|
16
|
+
query: { type: 'string', maxLength: 200 }, limit: { type: 'integer', minimum: 1, maximum: 50, default: 20 },
|
|
17
|
+
} } },
|
|
18
|
+
{ name: 'assistant_chat_read', sideEffects: 'none', authorization: { permission: 'admin.assistant.use' },
|
|
19
|
+
description: "Reads one of the CURRENT user's own chats in stored message order. Omit fromPosition to read the latest tail (default 50 messages); use fromPosition and nextPosition for forward pagination. List chats first when resuming earlier work, read the tail, summarise the state, then continue. Returns redacted messages, positions, continuation and truncated flags; each serialized message is capped at 8,000 characters and the result at 40,000 characters. Tool events, when requested, contain only tool name and outcome, never payloads. Attachments are omitted. Missing and other-user chats receive the same refusal.",
|
|
20
|
+
inputSchema: { type: 'object', additionalProperties: false, required: ['chatId'], properties: {
|
|
21
|
+
chatId: { type: 'string', minLength: 1, maxLength: 128 },
|
|
22
|
+
fromPosition: { type: 'integer', minimum: 0 }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 50 },
|
|
23
|
+
includeToolEvents: { type: 'boolean', default: false },
|
|
24
|
+
} } },
|
|
25
|
+
] });
|
|
26
|
+
/** Cap a fresh projection including JSON escapes and tool summaries, before the total-page cap.
|
|
27
|
+
* The projection is local to this call, so trimming it cannot mutate stored messages or events.
|
|
28
|
+
* @complexity Bounded content/summary serialization; no I/O or unbounded output accumulation.
|
|
29
|
+
*/
|
|
30
|
+
function capMessage({ view }, _options = {}) {
|
|
31
|
+
while (JSON.stringify(view).length > HISTORY_MESSAGE_CHAR_CAP) {
|
|
32
|
+
view.truncated = true;
|
|
33
|
+
if (view.content.length > 0)
|
|
34
|
+
view.content = view.content.slice(0, Math.floor(view.content.length / 2));
|
|
35
|
+
else
|
|
36
|
+
view.toolEvents?.pop();
|
|
37
|
+
}
|
|
38
|
+
return view;
|
|
39
|
+
}
|
|
40
|
+
/** Only bounded names and status cross this boundary; malformed historical events are ignored.
|
|
41
|
+
* @complexity O(e) time/space in persisted events; output is capped at 50 summaries.
|
|
42
|
+
*/
|
|
43
|
+
function summarizeTools({ events, redact }, _options = {}) {
|
|
44
|
+
if (!Array.isArray(events))
|
|
45
|
+
return { items: [], truncated: false };
|
|
46
|
+
const outcomes = new Map();
|
|
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
|
+
}
|
|
53
|
+
const items = [];
|
|
54
|
+
let truncated = false;
|
|
55
|
+
for (const raw of events) {
|
|
56
|
+
const event = raw;
|
|
57
|
+
if (event?.kind !== 'tool_use' || typeof event.name !== 'string')
|
|
58
|
+
continue;
|
|
59
|
+
if (items.length === TOOL_EVENT_CAP) {
|
|
60
|
+
truncated = true;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
const name = redact({ text: event.name }).text;
|
|
64
|
+
if (name.length > 120)
|
|
65
|
+
truncated = true;
|
|
66
|
+
items.push({ name: name.slice(0, 120), outcome: outcomes.get(event.id ?? '') ?? 'unknown' });
|
|
67
|
+
}
|
|
68
|
+
return { items, truncated };
|
|
69
|
+
}
|
|
70
|
+
/** Validate advertised integer bounds even when a transport bypasses JSON schema validation. */
|
|
71
|
+
function integerOption({ input, key, fallback, max, min = 1 }, _options = {}) {
|
|
72
|
+
const value = input[key] === undefined ? fallback : input[key];
|
|
73
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < min || value > max) {
|
|
74
|
+
throw new ToolInputError({ message: `${key} must be an integer from ${min} through ${max}.` });
|
|
75
|
+
}
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
/** Strict allowlist prevents model-supplied identity from ever reaching the host scope factory. */
|
|
79
|
+
function inputRecord({ input, keys }, _options = {}) {
|
|
80
|
+
const record = requireInputRecord({ input });
|
|
81
|
+
if (Object.keys(record).some(key => !keys.includes(key)))
|
|
82
|
+
throw new ToolInputError({ message: 'Unexpected chat history input field. Use the published schema.' });
|
|
83
|
+
return record;
|
|
84
|
+
}
|
|
85
|
+
/** Build both history registrations, using the existing catalog/risk/metadata contract.
|
|
86
|
+
* @param ports Authenticated scoped-store port; denied/missing history must fail closed.
|
|
87
|
+
* @param options Optional stricter redaction policy (the secret-card policy is the default).
|
|
88
|
+
* @returns Read-only tool registrations. @throws ToolInputError for malformed or refused reads.
|
|
89
|
+
* @complexity Bounded projections; persistence owns search cost and message ordering.
|
|
90
|
+
*/
|
|
91
|
+
export function buildAssistantHistoryRegistrations({ ports }, { redact = redactUserText } = {}) {
|
|
92
|
+
return buildDomainRegistrations({ domain: 'assistant-history', catalogModule: 'chat/store/runtime/history-tools', catalog: assistantHistoryToolCatalog,
|
|
93
|
+
derivedRisk: assistantHistoryDerivedRisk, metadata: assistantHistoryToolMetadata,
|
|
94
|
+
handlers: {
|
|
95
|
+
assistant_chats_list: async (ctx) => {
|
|
96
|
+
const input = inputRecord({ input: ctx.input, keys: ['query', 'limit'] });
|
|
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
|
+
},
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=history-tools.js.map
|
package/dist/store/sql/store.js
CHANGED
|
@@ -212,6 +212,54 @@ export function createSqlChatStore({ kernel, scope: inputScope }, { clock = crea
|
|
|
212
212
|
return row?.max === null || row?.max === undefined ? 0 : Number(row.max) + 1;
|
|
213
213
|
}
|
|
214
214
|
const store = {
|
|
215
|
+
history: {
|
|
216
|
+
/** Literal search uses two scoped projections, not a transcript fetch per conversation.
|
|
217
|
+
* @complexity The database scans owned content for search; only limit rows cross the port.
|
|
218
|
+
*/
|
|
219
|
+
async list(_required, { query, limit = 20 } = {}) {
|
|
220
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 50 || (query !== undefined && (typeof query !== 'string' || query.length > 200))) {
|
|
221
|
+
throw new ChatStoreError({ code: 'invalid-input' });
|
|
222
|
+
}
|
|
223
|
+
const rows = await run(db => {
|
|
224
|
+
let statement = ownedConversations(db).select(eb => eb.selectFrom('ai_chat_messages as first')
|
|
225
|
+
.select('first.content').whereRef('first.conversation_id', '=', 'c.id').where('first.role', '=', 'user')
|
|
226
|
+
.orderBy('first.position').limit(1).as('first_user_line'));
|
|
227
|
+
if (query?.trim()) {
|
|
228
|
+
// Escape LIKE wildcards: user search is literal text, never an SQL/FTS expression.
|
|
229
|
+
const pattern = '%' + query.trim().toLowerCase().replace(/[!%_]/g, char => '!' + char) + '%';
|
|
230
|
+
statement = statement.where(eb => eb.or([
|
|
231
|
+
sql `lower(c.title) like ${pattern} escape '!'`,
|
|
232
|
+
eb.exists(eb.selectFrom('ai_chat_messages as found').select('found.id')
|
|
233
|
+
.whereRef('found.conversation_id', '=', 'c.id')
|
|
234
|
+
.where(sql `lower(found.content) like ${pattern} escape '!'`)),
|
|
235
|
+
]));
|
|
236
|
+
}
|
|
237
|
+
return statement.orderBy('c.updated_at', 'desc').orderBy(ordinalId(kernel.dialect, 'c.id')).limit(limit).execute();
|
|
238
|
+
});
|
|
239
|
+
return rows.map(row => ({ ...toConversation(row), firstUserLine: row.first_user_line ?? '' }));
|
|
240
|
+
},
|
|
241
|
+
/** No attachment column is read. O(limit) rows; default tail is reversed into position order. */
|
|
242
|
+
async read({ chatId }, { fromPosition, limit = 50, includeToolEvents = false } = {}) {
|
|
243
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100 || (fromPosition !== undefined && (!Number.isSafeInteger(fromPosition) || fromPosition < 0))) {
|
|
244
|
+
throw new ChatStoreError({ code: 'invalid-input' });
|
|
245
|
+
}
|
|
246
|
+
const rows = await run(db => {
|
|
247
|
+
let statement = ownedMessages(db, chatId).clearSelect().select([
|
|
248
|
+
'm.id', 'm.role', 'm.content', 'm.position', 'm.created_at',
|
|
249
|
+
]);
|
|
250
|
+
if (fromPosition !== undefined)
|
|
251
|
+
statement = statement.where('m.position', '>=', fromPosition);
|
|
252
|
+
return statement.select(includeToolEvents ? sql `m.events_json`.as('events_json') : sql `null`.as('events_json'))
|
|
253
|
+
.orderBy('m.position', fromPosition === undefined ? 'desc' : 'asc').limit(limit + 1).execute();
|
|
254
|
+
});
|
|
255
|
+
const page = rows.slice(0, limit);
|
|
256
|
+
if (fromPosition === undefined)
|
|
257
|
+
page.reverse();
|
|
258
|
+
return { messages: page.map(row => ({ id: row.id, role: row.role, content: row.content,
|
|
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 };
|
|
261
|
+
},
|
|
262
|
+
},
|
|
215
263
|
async list() {
|
|
216
264
|
const rows = await run((db) => ownedConversations(db).orderBy("c.updated_at", "desc").execute());
|
|
217
265
|
return rows.map((row) => toConversation(row));
|
|
@@ -332,6 +380,10 @@ export function createSqlChatStore({ kernel, scope: inputScope }, { clock = crea
|
|
|
332
380
|
},
|
|
333
381
|
};
|
|
334
382
|
return {
|
|
383
|
+
history: {
|
|
384
|
+
list: (required, optional = {}) => atStoreBoundary({ operation: () => store.history.list(required, optional) }),
|
|
385
|
+
read: (required, optional = {}) => atStoreBoundary({ operation: () => store.history.read(required, optional) }),
|
|
386
|
+
},
|
|
335
387
|
list: () => atStoreBoundary({ operation: () => store.list() }),
|
|
336
388
|
get: (required, optional = {}) => atStoreBoundary({ operation: () => store.get(required, optional) }),
|
|
337
389
|
create: (required, optional = {}) => atStoreBoundary({ operation: () => store.create(required, optional) }),
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ChatMessage } from '../core/index.js';
|
|
2
|
+
/** `turns` user/assistant pairs; each reply carries `toolRowsPerTurn` finished Bash calls with output. */
|
|
3
|
+
export declare function createLongTranscriptFixture({ turns, toolRowsPerTurn }: {
|
|
4
|
+
turns: number;
|
|
5
|
+
toolRowsPerTurn: number;
|
|
6
|
+
}, _options?: Record<string, never>): ChatMessage[];
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** `turns` user/assistant pairs; each reply carries `toolRowsPerTurn` finished Bash calls with output. */
|
|
2
|
+
export function createLongTranscriptFixture({ turns, toolRowsPerTurn }, _options = {}) {
|
|
3
|
+
return Array.from({ length: turns }, (_, turn) => [
|
|
4
|
+
{ id: `u${turn}`, role: 'user', content: `Step ${turn}`, createdAt: turn * 2 },
|
|
5
|
+
{
|
|
6
|
+
id: `a${turn}`,
|
|
7
|
+
role: 'assistant',
|
|
8
|
+
content: `Done with step ${turn}.`,
|
|
9
|
+
runStatus: 'succeeded',
|
|
10
|
+
createdAt: turn * 2 + 1,
|
|
11
|
+
events: [
|
|
12
|
+
...Array.from({ length: toolRowsPerTurn }, (_, i) => [
|
|
13
|
+
{ kind: 'tool_use', id: `t${turn}-${i}`, name: 'Bash', input: { command: `ls ${i}` } },
|
|
14
|
+
{ kind: 'tool_result', toolUseId: `t${turn}-${i}`, content: 'file\n'.repeat(50), isError: false },
|
|
15
|
+
]).flat(),
|
|
16
|
+
{ kind: 'text', text: `Done with step ${turn}.` },
|
|
17
|
+
{ kind: 'ext', name: 'render-probe', data: { turn } },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
]).flat();
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/chat",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.8",
|
|
4
4
|
"description": "Jini's chat/artifact frontend, consolidated into one package with two subpaths: framework-free chat vocabulary and pure parsers in `/core`, headless hooks + presentational components + slots in `/react`. Import only the layer you need.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"runtime": "universal",
|
|
15
15
|
"entries": {
|
|
16
16
|
".": "universal",
|
|
17
|
+
"./testing": "universal",
|
|
17
18
|
"./core": "universal",
|
|
18
19
|
"./react": "browser",
|
|
19
20
|
"./react/chat-pane": "browser",
|
|
@@ -34,7 +35,10 @@
|
|
|
34
35
|
"./browser/session-store": "browser",
|
|
35
36
|
"./browser/page-actions": "browser",
|
|
36
37
|
"./server/durable-runs": "node",
|
|
37
|
-
"./store/runtime": "node"
|
|
38
|
+
"./store/runtime": "node",
|
|
39
|
+
"./vanilla": "browser",
|
|
40
|
+
"./browser/lazy-loader": "browser",
|
|
41
|
+
"./vanilla/style.css": "browser"
|
|
38
42
|
},
|
|
39
43
|
"neutralEntries": [
|
|
40
44
|
"src/store/contracts"
|
|
@@ -54,6 +58,11 @@
|
|
|
54
58
|
"main": "./dist/core/index.js",
|
|
55
59
|
"types": "./dist/core/index.d.ts",
|
|
56
60
|
"exports": {
|
|
61
|
+
"./testing": {
|
|
62
|
+
"types": "./dist/testing/index.d.ts",
|
|
63
|
+
"import": "./dist/testing/index.js",
|
|
64
|
+
"default": "./dist/testing/index.js"
|
|
65
|
+
},
|
|
57
66
|
".": {
|
|
58
67
|
"types": "./dist/core/index.d.ts",
|
|
59
68
|
"import": "./dist/core/index.js",
|
|
@@ -155,7 +164,18 @@
|
|
|
155
164
|
"types": "./dist/store/runtime/entry.d.ts",
|
|
156
165
|
"import": "./dist/store/runtime/entry.js",
|
|
157
166
|
"default": "./dist/store/runtime/entry.js"
|
|
158
|
-
}
|
|
167
|
+
},
|
|
168
|
+
"./vanilla": {
|
|
169
|
+
"types": "./dist/embed-widget/vanilla/index.d.ts",
|
|
170
|
+
"import": "./dist/embed-widget/vanilla/index.js",
|
|
171
|
+
"default": "./dist/embed-widget/vanilla/index.js"
|
|
172
|
+
},
|
|
173
|
+
"./browser/lazy-loader": {
|
|
174
|
+
"types": "./dist/embed-widget/browser/lazy-loader.d.ts",
|
|
175
|
+
"import": "./dist/embed-widget/browser/lazy-loader.js",
|
|
176
|
+
"default": "./dist/embed-widget/browser/lazy-loader.js"
|
|
177
|
+
},
|
|
178
|
+
"./vanilla/style.css": "./dist/embed-widget/vanilla/style.css"
|
|
159
179
|
},
|
|
160
180
|
"files": [
|
|
161
181
|
"dist",
|
|
@@ -176,12 +196,12 @@
|
|
|
176
196
|
"registry": "https://registry.npmjs.org"
|
|
177
197
|
},
|
|
178
198
|
"dependencies": {
|
|
179
|
-
"@jini-ai/core": "^0.4.
|
|
199
|
+
"@jini-ai/core": "^0.4.3"
|
|
180
200
|
},
|
|
181
201
|
"peerDependencies": {
|
|
182
202
|
"react": "^18.3.0 || ^19.0.0",
|
|
183
203
|
"react-dom": "^18.3.0 || ^19.0.0",
|
|
184
|
-
"@jini-ai/db": "^0.
|
|
204
|
+
"@jini-ai/db": "^0.3.2",
|
|
185
205
|
"kysely": "^0.29.6",
|
|
186
206
|
"better-sqlite3": "^13.0.0",
|
|
187
207
|
"@electric-sql/pglite": "0.5.8",
|
|
@@ -194,11 +214,11 @@
|
|
|
194
214
|
"@radix-ui/react-select": "^2.3.7",
|
|
195
215
|
"@radix-ui/react-slot": "^1.3.3",
|
|
196
216
|
"recharts": "^3.10.1",
|
|
197
|
-
"@jini-ai/agent-runtime": "^0.4.
|
|
198
|
-
"@jini-ai/agentic": "^0.4.
|
|
199
|
-
"@jini-ai/protocol": "^0.4.
|
|
200
|
-
"@jini-ai/ui": "^0.4.
|
|
201
|
-
"@jini-ai/diagnostics": "^0.5.
|
|
217
|
+
"@jini-ai/agent-runtime": "^0.4.5",
|
|
218
|
+
"@jini-ai/agentic": "^0.4.4",
|
|
219
|
+
"@jini-ai/protocol": "^0.4.4",
|
|
220
|
+
"@jini-ai/ui": "^0.4.8",
|
|
221
|
+
"@jini-ai/diagnostics": "^0.5.2"
|
|
202
222
|
},
|
|
203
223
|
"peerDependenciesMeta": {
|
|
204
224
|
"react": {
|
|
@@ -277,6 +297,7 @@
|
|
|
277
297
|
"@types/react": "^19.2.0",
|
|
278
298
|
"@types/react-dom": "^19.2.0",
|
|
279
299
|
"@vitest/coverage-v8": "^2.1.9",
|
|
300
|
+
"zod": "^3.25.76",
|
|
280
301
|
"jsdom": "^25.0.1",
|
|
281
302
|
"react": "^19.2.0",
|
|
282
303
|
"react-dom": "^19.2.0",
|
|
@@ -286,16 +307,19 @@
|
|
|
286
307
|
"@electric-sql/pglite": "0.5.8",
|
|
287
308
|
"pg": "8.23.0",
|
|
288
309
|
"@ag-ui/core": "0.0.58",
|
|
289
|
-
"@jini-ai/
|
|
290
|
-
"@jini-ai/
|
|
291
|
-
"@jini-ai/protocol": "^0.4.
|
|
292
|
-
"@jini-ai/diagnostics": "^0.5.
|
|
293
|
-
"@jini-ai/
|
|
294
|
-
"@jini-ai/
|
|
295
|
-
"@jini-ai/
|
|
310
|
+
"@jini-ai/db": "0.3.2",
|
|
311
|
+
"@jini-ai/agentic": "^0.4.4",
|
|
312
|
+
"@jini-ai/protocol": "^0.4.4",
|
|
313
|
+
"@jini-ai/diagnostics": "^0.5.2",
|
|
314
|
+
"@jini-ai/agent-runtime": "0.4.5",
|
|
315
|
+
"@jini-ai/core": "^0.4.3",
|
|
316
|
+
"@jini-ai/ui": "^0.4.8"
|
|
296
317
|
},
|
|
297
318
|
"typesVersions": {
|
|
298
319
|
"*": {
|
|
320
|
+
"testing": [
|
|
321
|
+
"./dist/testing/index.d.ts"
|
|
322
|
+
],
|
|
299
323
|
"core": [
|
|
300
324
|
"./dist/core/index.d.ts"
|
|
301
325
|
],
|
|
@@ -349,11 +373,17 @@
|
|
|
349
373
|
],
|
|
350
374
|
"store/runtime": [
|
|
351
375
|
"./dist/store/runtime/entry.d.ts"
|
|
376
|
+
],
|
|
377
|
+
"vanilla": [
|
|
378
|
+
"./dist/embed-widget/vanilla/index.d.ts"
|
|
379
|
+
],
|
|
380
|
+
"browser/lazy-loader": [
|
|
381
|
+
"./dist/embed-widget/browser/lazy-loader.d.ts"
|
|
352
382
|
]
|
|
353
383
|
}
|
|
354
384
|
},
|
|
355
385
|
"scripts": {
|
|
356
|
-
"build": "tsc -p tsconfig.json && mkdir -p dist/react/styles && cp src/react/styles/reference.css dist/react/styles/",
|
|
386
|
+
"build": "tsc -p tsconfig.json && mkdir -p dist/react/styles && cp src/react/styles/reference.css dist/react/styles/ && mkdir -p dist/embed-widget/vanilla && node --input-type=module -e \"import {writeFileSync,readFileSync} from 'node:fs'; import {CHAT_BUBBLE_STYLE} from './dist/embed-widget/browser/lazy-loader.js'; writeFileSync('dist/embed-widget/vanilla/style.css',CHAT_BUBBLE_STYLE+'\\n'+readFileSync('src/embed-widget/vanilla/style.css','utf8'));\"",
|
|
357
387
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
358
388
|
"test": "vitest run",
|
|
359
389
|
"test:coverage": "vitest run --coverage",
|