@jini-ai/chat 0.5.4 → 0.5.6
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 +8 -0
- package/dist/core/ag-ui/entry.d.ts +33 -0
- package/dist/core/ag-ui/entry.js +80 -0
- package/dist/core/agentic/chat-capabilities.d.ts +2 -4
- package/dist/core/agentic/index.d.ts +4 -17
- package/dist/core/agentic/index.js +4 -17
- package/dist/core/assistant-client/a2ui-action-poster.d.ts +46 -0
- package/dist/core/assistant-client/a2ui-action-poster.js +113 -0
- package/dist/core/assistant-client/ag-ui-transport.d.ts +68 -0
- package/dist/core/assistant-client/ag-ui-transport.js +147 -0
- package/dist/core/assistant-client/attachment-liveness.d.ts +37 -0
- package/dist/core/assistant-client/attachment-liveness.js +93 -0
- package/dist/core/assistant-client/durable-run-subscription.d.ts +39 -0
- package/dist/core/assistant-client/durable-run-subscription.js +96 -0
- package/dist/core/assistant-client/entry.d.ts +13 -0
- package/dist/core/assistant-client/entry.js +7 -0
- package/dist/core/assistant-client/messages.d.ts +73 -0
- package/dist/core/assistant-client/messages.js +68 -0
- package/dist/core/assistant-client/transport.d.ts +164 -0
- package/dist/core/assistant-client/transport.js +734 -0
- package/dist/core/browser/page-actions.d.ts +121 -0
- package/dist/core/browser/page-actions.js +113 -0
- package/dist/core/durable-runs/continuation.d.ts +18 -0
- package/dist/core/durable-runs/continuation.js +61 -0
- package/dist/core/durable-runs/ledger-contracts.d.ts +53 -0
- package/dist/core/durable-runs/ledger-contracts.js +2 -0
- package/dist/core/durable-runs/policies.d.ts +6 -0
- package/dist/core/durable-runs/policies.js +6 -0
- package/dist/core/durable-runs/ports.d.ts +123 -0
- package/dist/core/durable-runs/ports.js +2 -0
- package/dist/core/durable-runs/run-contracts.d.ts +17 -0
- package/dist/core/durable-runs/run-contracts.js +2 -0
- package/dist/core/durable-runs/run-lock.d.ts +4 -0
- package/dist/core/durable-runs/run-lock.js +4 -0
- package/dist/core/index.d.ts +6 -19
- package/dist/core/index.js +6 -19
- package/dist/core/messages.d.ts +2 -0
- package/dist/core/question-form/format.d.ts +0 -9
- package/dist/core/question-form/format.js +16 -2
- package/dist/core/question-form/normalize.js +1 -0
- package/dist/core/question-form/secret-answers.d.ts +38 -0
- package/dist/core/question-form/secret-answers.js +45 -0
- package/dist/core/question-form/types.d.ts +2 -0
- package/dist/core/question-form.d.ts +1 -0
- package/dist/core/question-form.js +1 -0
- package/dist/core/run-events/entry.d.ts +208 -16
- package/dist/core/run-events/entry.js +273 -39
- package/dist/core/transcript.d.ts +3 -2
- package/dist/core/transcript.js +19 -3
- package/dist/core/user-text-redaction.d.ts +56 -0
- package/dist/core/user-text-redaction.js +89 -0
- package/dist/embed-widget/browser/page-actions.d.ts +1 -120
- package/dist/embed-widget/browser/page-actions.js +2 -112
- package/dist/embed-widget/react/embed.d.ts +1 -1
- package/dist/embed-widget/react/embed.js +1 -1
- package/dist/react/artifact-types.d.ts +9 -15
- package/dist/react/components/QuestionForm.d.ts +2 -1
- package/dist/react/components/QuestionForm.js +18 -12
- package/dist/react/components/QuestionsPanel.d.ts +3 -2
- package/dist/react/components/QuestionsPanel.js +2 -2
- package/dist/react/features/chat-pane/agent-tools.d.ts +1 -1
- package/dist/react/features/chat-pane/agent-tools.js +0 -1
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +5 -2
- package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
- package/dist/react/features/chat-pane/components/ChatPane.js +5 -2
- package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +2 -1
- package/dist/react/features/chat-pane/create-typed-answer-poster.js +33 -1
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +3 -2
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +4 -1
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +43 -22
- package/dist/react/features/chat-pane/model-options.d.ts +12 -0
- package/dist/react/features/chat-pane/model-options.js +55 -0
- package/dist/react/features/chat-pane/rules.d.ts +3 -2
- package/dist/react/features/chat-pane/rules.js +12 -4
- package/dist/react/features/chat-pane/types.d.ts +25 -1
- package/dist/react/hooks/assistant-chats-contracts.d.ts +164 -0
- package/dist/react/hooks/assistant-chats-contracts.js +2 -0
- package/dist/react/hooks/assistant-chats.d.ts +7 -0
- package/dist/react/hooks/assistant-chats.js +960 -0
- package/dist/react/hooks/attachment-preview-cache.d.ts +1 -1
- package/dist/react/hooks/attachment-preview-cache.js +1 -1
- package/dist/react/hooks/composer-draft-cache.d.ts +2 -2
- package/dist/react/hooks/composer-draft-cache.js +2 -2
- package/dist/react/hooks/fab-position.d.ts +90 -0
- package/dist/react/hooks/fab-position.js +271 -0
- package/dist/react/hooks/useConversation.d.ts +4 -1
- package/dist/react/hooks/useConversation.js +14 -12
- package/dist/react/hooks/useUserTextGuard.d.ts +11 -0
- package/dist/react/hooks/useUserTextGuard.js +19 -0
- package/dist/react/index.d.ts +12 -21
- package/dist/react/index.js +7 -0
- package/dist/server/durable-runs/accept.d.ts +25 -0
- package/dist/server/durable-runs/accept.js +21 -0
- package/dist/server/durable-runs/entry.d.ts +5 -0
- package/dist/server/durable-runs/entry.js +4 -0
- package/dist/server/durable-runs/recover.d.ts +5 -0
- package/dist/server/durable-runs/recover.js +147 -0
- package/dist/server/run-finalizer/ports.d.ts +2 -16
- package/dist/store/legacy/sqlite/messages.d.ts +24 -18
- package/dist/store/legacy/sqlite/messages.js +12 -2
- package/dist/store/pglite/index.d.ts +2 -1
- package/dist/store/postgres/index.d.ts +2 -1
- package/dist/store/runtime/chat-expiry-sweep.d.ts +40 -0
- package/dist/store/runtime/chat-expiry-sweep.js +70 -0
- package/dist/store/runtime/chat-history-store.d.ts +20 -0
- package/dist/store/runtime/chat-history-store.js +50 -0
- package/dist/store/runtime/durable-run-store.d.ts +21 -0
- package/dist/store/runtime/durable-run-store.js +342 -0
- package/dist/store/runtime/entry.d.ts +7 -0
- package/dist/store/runtime/entry.js +6 -0
- package/dist/store/runtime/kernel.d.ts +29 -0
- package/dist/store/runtime/kernel.js +2 -0
- package/dist/store/runtime/run-guarded-chat-store.d.ts +10 -0
- package/dist/store/runtime/run-guarded-chat-store.js +42 -0
- package/dist/store/runtime/run-ledger.d.ts +15 -0
- package/dist/store/runtime/run-ledger.js +206 -0
- package/dist/store/sql/boundary.d.ts +6 -0
- package/dist/store/sql/boundary.js +7 -1
- package/dist/store/sql/store.d.ts +3 -2
- package/dist/store/sql/store.js +9 -2
- package/dist/store/sqlite/index.d.ts +3 -2
- package/package.json +75 -10
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module question-form/format
|
|
3
|
+
*
|
|
4
|
+
* Answer presentation: turning a submitted answer set into the prose the
|
|
5
|
+
* agent reads on its next turn, plus the label/value lookups also used by
|
|
6
|
+
* `normalize.ts`'s default-value resolution. Split out of the original
|
|
7
|
+
* single-file `question-form.ts`; see `../question-form.ts` for the public
|
|
8
|
+
* facade.
|
|
9
|
+
*/
|
|
10
|
+
import { redactQuestionAnswers, isSecretQuestion } from './secret-answers.js';
|
|
11
|
+
import { CREDENTIAL_CARD_GUIDANCE, redactUserText } from '../user-text-redaction.js';
|
|
1
12
|
/**
|
|
2
13
|
* Format a finished set of answers into a prose user message the agent can
|
|
3
14
|
* read on its next turn. The shape is stable enough that the agent can
|
|
@@ -6,10 +17,11 @@
|
|
|
6
17
|
* @complexity O(q) in the number of questions on the form.
|
|
7
18
|
*/
|
|
8
19
|
export function formatFormAnswers({ form, answers }) {
|
|
20
|
+
const safe = redactQuestionAnswers({ form, answers });
|
|
9
21
|
const lines = [];
|
|
10
22
|
lines.push(`[form answers — ${form.id}]`);
|
|
11
23
|
for (const q of form.questions) {
|
|
12
|
-
const v = answers[q.id];
|
|
24
|
+
const v = safe.answers[q.id];
|
|
13
25
|
let display;
|
|
14
26
|
if (Array.isArray(v)) {
|
|
15
27
|
display = v.length > 0 ? v.map((value) => formOptionDisplayForValue(q, value)).join(', ') : '(skipped)';
|
|
@@ -22,7 +34,9 @@ export function formatFormAnswers({ form, answers }) {
|
|
|
22
34
|
}
|
|
23
35
|
lines.push(`- ${q.label}: ${display}`);
|
|
24
36
|
}
|
|
25
|
-
|
|
37
|
+
if (safe.secretRedacted || form.questions.some(question => isSecretQuestion({ question })))
|
|
38
|
+
lines.push(CREDENTIAL_CARD_GUIDANCE);
|
|
39
|
+
return redactUserText({ text: lines.join('\n') }).text;
|
|
26
40
|
}
|
|
27
41
|
function formOptionDisplayForValue(question, value) {
|
|
28
42
|
const match = question.options?.find((option) => option.value === value || option.label === value);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Chat question forms are model-visible text, never secure credential storage. */
|
|
2
|
+
import { type UserTextRedactionOptions } from '../user-text-redaction.js';
|
|
3
|
+
import type { FormQuestion, QuestionForm } from './types.js';
|
|
4
|
+
export declare function isSecretQuestion({ question }: {
|
|
5
|
+
readonly question: Pick<FormQuestion, 'id' | 'label' | 'type' | 'name'>;
|
|
6
|
+
}): boolean;
|
|
7
|
+
/** The map as well as its formatted transcript must be safe: hosts often persist both. */
|
|
8
|
+
export declare function redactQuestionAnswers({ form, answers }: {
|
|
9
|
+
readonly form: QuestionForm;
|
|
10
|
+
readonly answers: Readonly<Record<string, string | string[]>>;
|
|
11
|
+
}, options?: UserTextRedactionOptions): {
|
|
12
|
+
readonly answers: Record<string, string | string[]>;
|
|
13
|
+
readonly secretRedacted: boolean;
|
|
14
|
+
readonly count: number;
|
|
15
|
+
};
|
|
16
|
+
/** Never populate a credential text question from model-supplied defaults. */
|
|
17
|
+
export declare function withoutSecretQuestionDefaults({ form }: {
|
|
18
|
+
readonly form: QuestionForm;
|
|
19
|
+
}): QuestionForm;
|
|
20
|
+
/** Attributes are presentation policy, shared by any host renderer. */
|
|
21
|
+
export declare function questionTextInputProps({ question }: {
|
|
22
|
+
readonly question: FormQuestion;
|
|
23
|
+
}): {
|
|
24
|
+
type: 'password' | 'text';
|
|
25
|
+
autoComplete?: string;
|
|
26
|
+
spellCheck?: boolean;
|
|
27
|
+
};
|
|
28
|
+
export declare function questionTextareaProps({ question }: {
|
|
29
|
+
readonly question: FormQuestion;
|
|
30
|
+
}): {
|
|
31
|
+
autoComplete?: string;
|
|
32
|
+
spellCheck?: boolean;
|
|
33
|
+
style?: {
|
|
34
|
+
WebkitTextSecurity: string;
|
|
35
|
+
whiteSpace: 'pre-wrap';
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=secret-answers.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Chat question forms are model-visible text, never secure credential storage. */
|
|
2
|
+
import { redactUserText } from '../user-text-redaction.js';
|
|
3
|
+
export function isSecretQuestion({ question }) {
|
|
4
|
+
if (question.type !== 'text' && question.type !== 'textarea')
|
|
5
|
+
return false;
|
|
6
|
+
const names = [question.id, question.label, question.name ?? ''].join(' ').replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
7
|
+
return /(?:^|[^a-z0-9])(?:key|token|password|passwd|secret)(?:$|[^a-z0-9])/i.test(names);
|
|
8
|
+
}
|
|
9
|
+
/** The map as well as its formatted transcript must be safe: hosts often persist both. */
|
|
10
|
+
export function redactQuestionAnswers({ form, answers }, options = {}) {
|
|
11
|
+
let count = 0;
|
|
12
|
+
const cleaned = {};
|
|
13
|
+
const redact = options.redactUserText ?? redactUserText;
|
|
14
|
+
for (const question of form.questions) {
|
|
15
|
+
const value = answers[question.id];
|
|
16
|
+
if (value === undefined)
|
|
17
|
+
continue;
|
|
18
|
+
const guard = (text) => {
|
|
19
|
+
const result = redact({ text }, { secretField: isSecretQuestion({ question }) });
|
|
20
|
+
count += result.count;
|
|
21
|
+
return result.text;
|
|
22
|
+
};
|
|
23
|
+
cleaned[question.id] = Array.isArray(value) ? value.map(guard) : guard(value);
|
|
24
|
+
}
|
|
25
|
+
if (count > 0)
|
|
26
|
+
options.onSecretRedacted?.({ secretRedacted: true, count });
|
|
27
|
+
return { answers: cleaned, secretRedacted: count > 0, count };
|
|
28
|
+
}
|
|
29
|
+
/** Never populate a credential text question from model-supplied defaults. */
|
|
30
|
+
export function withoutSecretQuestionDefaults({ form }) {
|
|
31
|
+
return { ...form, questions: form.questions.map(question => {
|
|
32
|
+
if (!isSecretQuestion({ question }))
|
|
33
|
+
return question;
|
|
34
|
+
const { defaultValue: _defaultValue, ...safe } = question;
|
|
35
|
+
return safe;
|
|
36
|
+
}) };
|
|
37
|
+
}
|
|
38
|
+
/** Attributes are presentation policy, shared by any host renderer. */
|
|
39
|
+
export function questionTextInputProps({ question }) {
|
|
40
|
+
return isSecretQuestion({ question }) ? { type: 'password', autoComplete: 'new-password', spellCheck: false } : { type: 'text' };
|
|
41
|
+
}
|
|
42
|
+
export function questionTextareaProps({ question }) {
|
|
43
|
+
return isSecretQuestion({ question }) ? { autoComplete: 'off', spellCheck: false, style: { WebkitTextSecurity: 'disc', whiteSpace: 'pre-wrap' } } : {};
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=secret-answers.js.map
|
|
@@ -36,4 +36,5 @@ export type { QuestionType, DirectionCard, FormOption, FormQuestion, QuestionFor
|
|
|
36
36
|
export { splitOnQuestionForms, findFirstQuestionForm, parseQuestionForm, stripTrailingOpenQuestionForm, hasUnterminatedQuestionForm, parsePartialQuestionForm, } from './question-form/scan.js';
|
|
37
37
|
export { isRenderableColor } from './question-form/normalize.js';
|
|
38
38
|
export { formatFormAnswers, formOptionLabelForValue, formOptionValueForLabel } from './question-form/format.js';
|
|
39
|
+
export { isSecretQuestion, redactQuestionAnswers, withoutSecretQuestionDefaults, questionTextInputProps, questionTextareaProps } from './question-form/secret-answers.js';
|
|
39
40
|
//# sourceMappingURL=question-form.d.ts.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { splitOnQuestionForms, findFirstQuestionForm, parseQuestionForm, stripTrailingOpenQuestionForm, hasUnterminatedQuestionForm, parsePartialQuestionForm, } from './question-form/scan.js';
|
|
2
2
|
export { isRenderableColor } from './question-form/normalize.js';
|
|
3
3
|
export { formatFormAnswers, formOptionLabelForValue, formOptionValueForLabel } from './question-form/format.js';
|
|
4
|
+
export { isSecretQuestion, redactQuestionAnswers, withoutSecretQuestionDefaults, questionTextInputProps, questionTextareaProps } from './question-form/secret-answers.js';
|
|
4
5
|
//# sourceMappingURL=question-form.js.map
|
|
@@ -4,6 +4,30 @@
|
|
|
4
4
|
* event names and excluded run-ID prefixes are explicit host inputs, never product wire defaults.
|
|
5
5
|
* Authenticated routing, ownership, boot repair and storage transactions remain host responsibilities. */
|
|
6
6
|
import type { AgentEvent } from "../events.js";
|
|
7
|
+
/**
|
|
8
|
+
* @file The ONE translation from an agent-daemon run stream to chat events, shared by the browser
|
|
9
|
+
* and the server.
|
|
10
|
+
*
|
|
11
|
+
* Purpose:
|
|
12
|
+
* A daemon run streams `@jini-ai/protocol` frames (`agent`/`stdout`/`stderr`/`error`/`end`). A saved
|
|
13
|
+
* chat row stores chat-core `AgentEvent`s plus a text `content` and a `run_status`. Until 2026-09-27
|
|
14
|
+
* the frame-to-event translation lived only in the admin SPA (`apps/admin/src/lib/assistant-transport.ts`),
|
|
15
|
+
* so only a connected browser could turn a run into a saved answer: close the tab, or lose it to a
|
|
16
|
+
* restart, and the row stayed `running` with no content forever (FINDING A,
|
|
17
|
+
* `ADS-memory/reports/2026-09-27-stuck-chat-root-cause.md`).
|
|
18
|
+
*
|
|
19
|
+
* The server-side finalizer (`server/runtime/composition/modules/assistant-run-finalizer.ts`) now
|
|
20
|
+
* saves the finished turn itself, and it must write exactly what the browser would have written. Two
|
|
21
|
+
* copies of this switch would drift, so both import this file through the shared
|
|
22
|
+
* `@jini-ai/chat/core/run-events` entry, with the host's bundler, test runner and TypeScript
|
|
23
|
+
* resolving the same package contract.
|
|
24
|
+
*
|
|
25
|
+
* Architectural role:
|
|
26
|
+
* PURE. No I/O, no DOM, no Node built-ins. The one value import is `@jini-ai/chat/core`'s content
|
|
27
|
+
* rule, itself pure and React-free, so the saved `content` and the browser's can never differ.
|
|
28
|
+
* `ReadableStream`/`TextDecoder` in {@link readSseFrames} are globals in both runtimes. Keep it that
|
|
29
|
+
* way — the admin bundle imports it.
|
|
30
|
+
*/
|
|
7
31
|
import type { ChatRunStatus } from "../messages.js";
|
|
8
32
|
export interface RunAgentPayload {
|
|
9
33
|
readonly type: string;
|
|
@@ -17,15 +41,53 @@ export interface RunProtocolEventWire {
|
|
|
17
41
|
/** Display a wire scalar or JSON value using the existing empty-null convention. */
|
|
18
42
|
export declare function asString({ value }: {
|
|
19
43
|
value: unknown;
|
|
20
|
-
}): string;
|
|
44
|
+
}, _options?: Record<string, never>): string;
|
|
45
|
+
/**
|
|
46
|
+
* Parses a `"usage"` wire payload into its renderable `AgentEvent`.
|
|
47
|
+
*
|
|
48
|
+
* The one case in {@link translateRunAgentPayload}'s switch with real branching: four independent
|
|
49
|
+
* optional fields, each `typeof`-checked before use (a malformed/missing field must not throw or
|
|
50
|
+
* silently coerce to `0`/`NaN`). Pulled out (2026-08-06, complexity pass, sixth pass) so those four
|
|
51
|
+
* ternaries are scored in their own scope instead of the switch's — this is the fix for the earlier
|
|
52
|
+
* `@complexityExemption`'s cognitive attribution on `translateRunAgentPayload`, which credited the
|
|
53
|
+
* `mcp-ui`/`a2ui` cases (each a one-line unwrap, zero branching — see their own comments below) for
|
|
54
|
+
* a cost that actually came from here.
|
|
55
|
+
*/
|
|
21
56
|
/** Keep only numeric usage fields; omit absent fields without changing JSON output. */
|
|
22
57
|
export declare function parseUsageEvent({ payload }: {
|
|
23
58
|
payload: RunAgentPayload;
|
|
24
|
-
}): AgentEvent;
|
|
59
|
+
}, _options?: Record<string, never>): AgentEvent;
|
|
60
|
+
/**
|
|
61
|
+
* Reduces one wire-level `RunAgentPayload` into zero or one renderable `AgentEvent`s.
|
|
62
|
+
*
|
|
63
|
+
* Exported (a pure function, so directly testable with no `EventSource`/`fetch` stub needed — see
|
|
64
|
+
* `assistant-transport.transcript.test.ts`'s own module doc for the same reasoning applied to
|
|
65
|
+
* `runPrompt`) so `assistant-transport.a2ui.test.ts` can assert the `"a2ui"` branch below in
|
|
66
|
+
* isolation.
|
|
67
|
+
*
|
|
68
|
+
* @complexityExemption (2026-08-06, complexity pass, sixth pass; bar is ≤9/≤9) **Score: 12
|
|
69
|
+
* cyclomatic / 3 cognitive. Bar: ≤9 cyclomatic AND ≤9 cognitive. Cyclomatic-only exemption —
|
|
70
|
+
* cognitive already clears the bar since {@link parseUsageEvent} above took the switch's only real
|
|
71
|
+
* branching out of this function's own scope.** (An earlier version of this comment attributed the
|
|
72
|
+
* cognitive cost to the `mcp-ui`/`a2ui` cases below; that was wrong — an independent audit measured
|
|
73
|
+
* that extracting `parseUsageEvent` alone drops cognitive from 11 to 3, which only makes sense if
|
|
74
|
+
* `usage` was the real source. `mcp-ui`/`a2ui` are one-line unwraps with zero branching, exactly as
|
|
75
|
+
* their own comments already said.) Cyclomatic stays over by construction, not by accident: this is
|
|
76
|
+
* a flat `switch` over `RunProtocolEventWire`'s closed `payload.type` vocabulary, one `case` per
|
|
77
|
+
* wire type, and cyclomatic counts every `case` as a branch regardless of shape. Tried and rejected:
|
|
78
|
+
* a `Record<string, (payload) => AgentEvent | null>` lookup table scores lower but loses two things
|
|
79
|
+
* a switch over a TS discriminated union keeps — exhaustiveness checking (a lookup table compiles
|
|
80
|
+
* with a missing key; this switch does not, once `payload.type` is narrowed to the real union rather
|
|
81
|
+
* than the wire's untyped `string`), and the ability to attach a multi-paragraph comment to one case
|
|
82
|
+
* explaining a specific interop bug (the `mcp-ui`/`a2ui` cases' comments each document why THAT case
|
|
83
|
+
* cannot fall through to `default` — see below). A lookup table would have to carry those as a
|
|
84
|
+
* parallel structure, one step removed from the code they explain. Left as a `switch`, documented
|
|
85
|
+
* here rather than only in this session's report.
|
|
86
|
+
*/
|
|
25
87
|
/** Reduce one daemon payload; preserve UI envelopes and drop duplicate input fragments. */
|
|
26
88
|
export declare function translateRunAgentPayload({ payload }: {
|
|
27
89
|
payload: RunAgentPayload;
|
|
28
|
-
}): AgentEvent | null;
|
|
90
|
+
}, _options?: Record<string, never>): AgentEvent | null;
|
|
29
91
|
/** Host-supplied status messages; no host branding or log path defaults. */
|
|
30
92
|
export interface RunNotices {
|
|
31
93
|
readonly toolStepLimit: Extract<AgentEvent, {
|
|
@@ -43,13 +105,32 @@ export interface RunNotices {
|
|
|
43
105
|
outcome: TerminalOutcome;
|
|
44
106
|
}) => string;
|
|
45
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Turns a terminal stream `reason` into the one renderable event a human needs to see, or `null`
|
|
110
|
+
* when the reason speaks for itself.
|
|
111
|
+
*
|
|
112
|
+
* Only `max_tool_turns` qualifies today, and it qualifies for a specific reason: it is the one
|
|
113
|
+
* terminal reason that is INDISTINGUISHABLE from success in the pane. `stop`/`end_turn` mean the
|
|
114
|
+
* assistant finished; an `error` reason already renders as an error. A turn that hit the tool-step
|
|
115
|
+
* ceiling just stops — mid-task, with whatever partial text it had, and nothing on screen saying
|
|
116
|
+
* the work was cut short rather than completed. That is the failure this exists to close: the
|
|
117
|
+
* server now reports the loop's real reason (`byok-provider-turn.ts`'s `normalizeTurnResult`
|
|
118
|
+
* captures it instead of echoing the provider's last raw stop code), and until this, the browser
|
|
119
|
+
* received that reason and dropped it.
|
|
120
|
+
*
|
|
121
|
+
* Rendered as a `status` event rather than an `error`, deliberately: nothing failed. The turn did
|
|
122
|
+
* real work and stopped at a budget, and the useful next action is "ask it to continue", which is
|
|
123
|
+
* what the detail says.
|
|
124
|
+
*/
|
|
46
125
|
/** A host notice for a run that used its tool-step budget. */
|
|
47
126
|
export declare function terminalReasonNotice({ reason, notice }: {
|
|
48
127
|
reason: string;
|
|
49
128
|
notice: Extract<AgentEvent, {
|
|
50
129
|
kind: "status";
|
|
51
130
|
}>;
|
|
52
|
-
}): AgentEvent | null;
|
|
131
|
+
}, _options?: Record<string, never>): AgentEvent | null;
|
|
132
|
+
/** A daemon `end` frame's non-successful terminal classification, as the daemon itself recorded it
|
|
133
|
+
* in `@jini-ai/protocol`'s `RunEndPayload`. */
|
|
53
134
|
export interface TerminalOutcome {
|
|
54
135
|
readonly status: "failed" | "canceled";
|
|
55
136
|
readonly code: string;
|
|
@@ -59,63 +140,174 @@ export interface TerminalOutcome {
|
|
|
59
140
|
/** Read non-successful daemon classification without throwing on malformed frames. */
|
|
60
141
|
export declare function readTerminalOutcome({ raw }: {
|
|
61
142
|
raw: string | undefined;
|
|
62
|
-
}): TerminalOutcome | null;
|
|
143
|
+
}, _options?: Record<string, never>): TerminalOutcome | null;
|
|
144
|
+
/**
|
|
145
|
+
* Turns a daemon `end` frame's terminal outcome into a visible `status` event when — and only when —
|
|
146
|
+
* the run did NOT succeed.
|
|
147
|
+
*
|
|
148
|
+
* WHY THIS EXISTS (2026-09-06 chat-death investigation, `ADS-memory/reports/2026-09-06-chat-death-investigation.md`):
|
|
149
|
+
* `@jini-ai/protocol`'s `RunEndPayload` carries `status`/`code`/`signal`/`resumable`, and
|
|
150
|
+
* `@jini-ai/daemon`'s `finish()` is the ONLY event a terminal run emits — there is no separate
|
|
151
|
+
* `error` frame for a failed run. {@link subscribeToRun}'s `end` listener read only `reason` (a
|
|
152
|
+
* field `RunEndPayload` does not even have — it is BYOK-only), so a run the daemon had already
|
|
153
|
+
* classified `failed` arrived here indistinguishable from a completed one, was reported through
|
|
154
|
+
* `onDone`, and was persisted to `chat.db` as `run_status='succeeded'` with empty content. Two such
|
|
155
|
+
* rows existed in the investigated host's `chat.db` — one `codex`, one `claude`, 578 ms and 552 ms — and they
|
|
156
|
+
* are what "the chat just craps out with no error" actually looks like on disk.
|
|
157
|
+
*
|
|
158
|
+
* This is the SEEN half of that fix; {@link terminalFailureError} below is the WRITTEN-DOWN half
|
|
159
|
+
* (2026-09-07, owner-approved). An earlier version of this comment said the persistence change was
|
|
160
|
+
* deliberately NOT made and was out of scope until the owner signed off — that is no longer true,
|
|
161
|
+
* and the two halves stayed separate functions only because they disagree on exactly one input:
|
|
162
|
+
* `canceled` earns a notice but is not a failure.
|
|
163
|
+
*
|
|
164
|
+
* Returns `null` for a successful or absent status so a normal turn gains no extra event.
|
|
165
|
+
*/
|
|
63
166
|
/** A visible terminal classification rendered with host-supplied copy. */
|
|
64
167
|
export declare function terminalOutcomeNotice({ raw, notices }: {
|
|
65
168
|
raw: string | undefined;
|
|
66
169
|
notices: RunNotices;
|
|
67
|
-
}): AgentEvent | null;
|
|
170
|
+
}, _options?: Record<string, never>): AgentEvent | null;
|
|
171
|
+
/**
|
|
172
|
+
* The reportable `Error` for a daemon `end` frame the daemon itself classified as `failed`, or
|
|
173
|
+
* `null` for every other terminal outcome.
|
|
174
|
+
*
|
|
175
|
+
* WHY THIS EXISTS (2026-09-07, owner-approved): it is what makes a dead run RECOVERABLE FROM THE
|
|
176
|
+
* DATABASE ALONE. Until now a failed run was reported only through `onDone`, so the durable record
|
|
177
|
+
* said `run_status='succeeded'` with empty content: the live transcript was the ONLY place the
|
|
178
|
+
* failure was visible, and once it was gone the row was indistinguishable from a successful turn
|
|
179
|
+
* that happened to answer with nothing. That lie is why diagnosing chat deaths burned multiple
|
|
180
|
+
* sessions and carried a wrong premise through two handoffs. `f682eff2` deliberately left it in
|
|
181
|
+
* place ("a behavior change to what the product writes down... out of scope until the owner signs
|
|
182
|
+
* off"); the owner has now signed off, and this is that change.
|
|
183
|
+
*
|
|
184
|
+
* NOTHING IN THIS FILE COMPUTES `runStatus`. Three pieces of `@jini-ai/chat` do, and the whole
|
|
185
|
+
* effect of this function rests on all three:
|
|
186
|
+
* 1. `useRunStream`'s `onError` sets the run's status to `'error'` — and its `onDone` is written
|
|
187
|
+
* as `prev.status === 'error' ? prev.status : 'done'`, so an `onDone` arriving AFTER this
|
|
188
|
+
* preserves the failure instead of overwriting it. That is what lets {@link subscribeToRun}
|
|
189
|
+
* report the failure and STILL settle the run through `finish()` with its collected events
|
|
190
|
+
* intact, rather than having to choose between the two. The call order in that listener is
|
|
191
|
+
* load-bearing, not incidental.
|
|
192
|
+
* 2. `useConversation` maps run status `'error'` to `ChatMessage.runStatus: 'failed'`.
|
|
193
|
+
* 3. `isTerminalRunStatus` already counts `'failed'` as terminal, so `assistant-chats.ts`'s
|
|
194
|
+
* `persistableMessages` KEEPS the message (it discards only still-streaming turns) and
|
|
195
|
+
* `AssistantDock/hooks/AssistantDock.hooks.tsx`'s `shouldPublishOnMessagesChange` still
|
|
196
|
+
* settles the dock. A failed run therefore cannot hang the pane waiting for a terminal state
|
|
197
|
+
* that never arrives — which is the failure this change would otherwise have traded the wrong
|
|
198
|
+
* record for.
|
|
199
|
+
*
|
|
200
|
+
* `canceled` is excluded on purpose: a run the operator stopped is not a failure, and
|
|
201
|
+
* `useRunStream.cancel()` already stamps its own `'canceled'` status. Marking it `failed` would
|
|
202
|
+
* swap one wrong record for another.
|
|
203
|
+
*
|
|
204
|
+
* Historical rows are NOT retrofitted. The investigated host's `chat.db` held 2 such rows on 2026-09-07 and
|
|
205
|
+
* the pre-split `content.db` copy held 6; nothing distinguishes a genuinely empty
|
|
206
|
+
* successful answer from a death after the fact, so a migration could only guess. Going-forward
|
|
207
|
+
* correctness is the goal.
|
|
208
|
+
*/
|
|
68
209
|
/** Report a failed run; operator cancellation is not a failure. */
|
|
69
210
|
export declare function terminalFailureError({ raw }: {
|
|
70
211
|
raw: string | undefined;
|
|
71
|
-
}): Error | null;
|
|
212
|
+
}, _options?: Record<string, never>): Error | null;
|
|
213
|
+
/** Reads a terminal frame's `reason` from either stream shape without letting a malformed or absent
|
|
214
|
+
* body prevent the turn from ending: the daemon path wraps it in a `RunProtocolEventWire.payload`,
|
|
215
|
+
* the BYOK path sends a bare `{reason}`, and `subscribeToRun`'s `end` event may carry no data at
|
|
216
|
+
* all. A notice is a nicety; finishing the run is not. */
|
|
72
217
|
/** Read a wrapped daemon or bare provider terminal reason, failing soft. */
|
|
73
218
|
export declare function readTerminalReason({ raw, wrapped }: {
|
|
74
219
|
raw: string | undefined;
|
|
75
220
|
wrapped: boolean;
|
|
76
|
-
}): string;
|
|
221
|
+
}, _options?: Record<string, never>): string;
|
|
222
|
+
/**
|
|
223
|
+
* Parses one blank-line-delimited SSE frame's raw text into `{event, data}`, split out as its own
|
|
224
|
+
* pure function so it is directly testable with a plain string, no `ReadableStream`/reader involved.
|
|
225
|
+
*
|
|
226
|
+
* Returns `null` for a frame with no `data:` lines — {@link readSseFrames} skips yielding those
|
|
227
|
+
* (a bare `event: ping` keepalive, for example, or a frame carrying only an `id:` field).
|
|
228
|
+
*/
|
|
77
229
|
/** Parse the existing named SSE framing; bare keepalive frames return null. */
|
|
78
230
|
export declare function parseFrame({ rawFrame }: {
|
|
79
231
|
rawFrame: string;
|
|
80
|
-
}): {
|
|
232
|
+
}, _options?: Record<string, never>): {
|
|
81
233
|
event: string;
|
|
82
234
|
data: string;
|
|
83
235
|
} | null;
|
|
84
|
-
/**
|
|
236
|
+
/**
|
|
237
|
+
* Splits a `text/event-stream` response body into `{event, data}` frames. Frames are
|
|
238
|
+
* blank-line-delimited per the SSE spec; per-frame field parsing lives in {@link parseFrame} above.
|
|
239
|
+
*
|
|
240
|
+
* @complexity O(n) in response body bytes; O(1) additional buffering per chunk beyond the
|
|
241
|
+
* not-yet-terminated tail of the current frame.
|
|
242
|
+
* @overallScore 100
|
|
243
|
+
*/
|
|
244
|
+
/** Decode complete named frames; cancellation always releases its reader. Inherits shared framing costs. */
|
|
85
245
|
export declare function readSseFrames({ body }: {
|
|
86
246
|
body: ReadableStream<Uint8Array>;
|
|
87
|
-
}): AsyncGenerator<{
|
|
247
|
+
}, _options?: Record<string, never>): AsyncGenerator<{
|
|
88
248
|
event: string;
|
|
89
249
|
data: string;
|
|
90
250
|
}>;
|
|
251
|
+
/**
|
|
252
|
+
* Whether the agent daemon holds this run, so its event stream can be read at
|
|
253
|
+
* `/api/runs/:id/events`. A BYOK or AG-UI run id names a turn that lived on one browser request, and
|
|
254
|
+
* there is nothing server-side to watch.
|
|
255
|
+
*/
|
|
91
256
|
/** Identify runs held by the daemon using the host's excluded ID prefixes. */
|
|
92
257
|
export declare function isDaemonRunId({ runId, excludedPrefixes }: {
|
|
93
258
|
runId: string;
|
|
94
259
|
excludedPrefixes: readonly string[];
|
|
95
|
-
}): boolean;
|
|
260
|
+
}, _options?: Record<string, never>): boolean;
|
|
96
261
|
/** Copy the host's interruption notice into a saved turn. */
|
|
97
262
|
export declare function runInterruptedNotice({ notice }: {
|
|
98
263
|
notice: Extract<AgentEvent, {
|
|
99
264
|
kind: "status";
|
|
100
265
|
}>;
|
|
101
|
-
}): AgentEvent;
|
|
266
|
+
}, _options?: Record<string, never>): AgentEvent;
|
|
267
|
+
/**
|
|
268
|
+
* A chat message's `content` for a list of events: `@jini-ai/chat`'s own rule
|
|
269
|
+
* (`assistantContentFromEvents`, also what `useConversation` uses for a live turn), so a row the
|
|
270
|
+
* server finalizes reads the same as one the browser saved. That rule puts a paragraph break where a
|
|
271
|
+
* tool call or card sat between two text runs, so a working note is never glued onto the answer.
|
|
272
|
+
*/
|
|
102
273
|
/** Use the same paragraph-boundary rule as a live chat turn. */
|
|
103
274
|
export declare function runContentFromEvents({ events }: {
|
|
104
275
|
events: readonly AgentEvent[];
|
|
105
|
-
}): string;
|
|
276
|
+
}, _options?: Record<string, never>): string;
|
|
277
|
+
/**
|
|
278
|
+
* The events to SAVE for a turn: streamed `text`/`thinking` deltas joined into one event per run
|
|
279
|
+
* (`@jini-ai/chat`'s `mergeAdjacentTextEvents`). Live, one event per few tokens is what streams words
|
|
280
|
+
* onto the screen; saved, it only repeats the event wrapper per token. Used by every saver (the server
|
|
281
|
+
* finalizer and the chat PUT route the browser saves through), so both write the same shape.
|
|
282
|
+
*/
|
|
106
283
|
/** Compact adjacent text/thinking deltas for durable storage. */
|
|
107
284
|
export declare function runEventsForSave({ events }: {
|
|
108
285
|
events: readonly AgentEvent[];
|
|
109
|
-
}): AgentEvent[];
|
|
286
|
+
}, _options?: Record<string, never>): AgentEvent[];
|
|
287
|
+
/** What one daemon stream frame means for the turn: events to append, a failure, and whether it ended. */
|
|
288
|
+
/** Set when the frame reports a failure. Once any frame has, the turn is `failed` (the browser's
|
|
289
|
+
* `useRunStream` keeps an `'error'` status through a later `onDone`). */
|
|
290
|
+
/** Set only on the `end` frame: the daemon's own terminal classification. */
|
|
110
291
|
export interface RunFrameOutcome {
|
|
111
292
|
readonly events: AgentEvent[];
|
|
112
293
|
readonly error?: Error;
|
|
113
294
|
readonly terminal?: Extract<ChatRunStatus, "succeeded" | "failed" | "canceled">;
|
|
114
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Translates one named daemon SSE frame (`event:` name plus raw `data:` text) into its effect on the
|
|
298
|
+
* turn. The browser's `subscribeToRun` dispatches the result to `RunHandlers`; the server finalizer
|
|
299
|
+
* folds it into the row it saves. Never throws: a malformed frame yields no events rather than
|
|
300
|
+
* stranding either reader mid-turn.
|
|
301
|
+
*
|
|
302
|
+
* An `error` frame with no data is a connection error, not a run error — the browser's
|
|
303
|
+
* `EventSource` reports those on the same event name, so it must check for data before calling this.
|
|
304
|
+
*
|
|
305
|
+
* @complexity O(size of the frame).
|
|
306
|
+
*/
|
|
115
307
|
/** Fold one named daemon frame into events, a failure and an optional terminal status. */
|
|
116
308
|
export declare function translateRunFrame({ kind, raw, notices }: {
|
|
117
309
|
kind: string;
|
|
118
310
|
raw: string | undefined;
|
|
119
311
|
notices: RunNotices;
|
|
120
|
-
}): RunFrameOutcome;
|
|
312
|
+
}, _options?: Record<string, never>): RunFrameOutcome;
|
|
121
313
|
//# sourceMappingURL=entry.d.ts.map
|