@things-factory/ai-assistant 10.1.228 → 10.1.232
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/client/components/assistant-chat.ts +27 -4
- package/client/components/assistant-dock.ts +18 -2
- package/client/components/proposal-outcome-line.ts +5 -0
- package/client/utils/applied-screens.ts +36 -0
- package/client/utils/assistant-proposals.ts +2 -0
- package/client/utils/assistant-selection.ts +3 -2
- package/client/utils/entry-proposal.ts +58 -3
- package/dist-client/components/assistant-chat.d.ts +3 -0
- package/dist-client/components/assistant-chat.js +27 -4
- package/dist-client/components/assistant-chat.js.map +1 -1
- package/dist-client/components/assistant-dock.js +18 -2
- package/dist-client/components/assistant-dock.js.map +1 -1
- package/dist-client/components/proposal-outcome-line.d.ts +2 -0
- package/dist-client/components/proposal-outcome-line.js +4 -0
- package/dist-client/components/proposal-outcome-line.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/applied-screens.d.ts +21 -0
- package/dist-client/utils/applied-screens.js +32 -0
- package/dist-client/utils/applied-screens.js.map +1 -0
- package/dist-client/utils/assistant-proposals.d.ts +2 -0
- package/dist-client/utils/assistant-proposals.js.map +1 -1
- package/dist-client/utils/assistant-selection.d.ts +1 -1
- package/dist-client/utils/assistant-selection.js +3 -2
- package/dist-client/utils/assistant-selection.js.map +1 -1
- package/dist-client/utils/entry-proposal.d.ts +22 -0
- package/dist-client/utils/entry-proposal.js +56 -3
- package/dist-client/utils/entry-proposal.js.map +1 -1
- package/dist-server/service/assistant-mode/index.d.ts +1 -0
- package/dist-server/service/assistant-mode/index.js +5 -1
- package/dist-server/service/assistant-mode/index.js.map +1 -1
- package/dist-server/service/assistant-mode/record-subject.d.ts +25 -0
- package/dist-server/service/assistant-mode/record-subject.js +35 -0
- package/dist-server/service/assistant-mode/record-subject.js.map +1 -0
- package/dist-server/service/chat-session/attachment-tool-result.d.ts +1 -0
- package/dist-server/service/chat-session/attachment-tool-result.js +11 -0
- package/dist-server/service/chat-session/attachment-tool-result.js.map +1 -1
- package/dist-server/service/chat-session/attachment-tools.js +5 -2
- package/dist-server/service/chat-session/attachment-tools.js.map +1 -1
- package/dist-server/service/data-entry/entry-screen.d.ts +10 -0
- package/dist-server/service/data-entry/entry-screen.js +18 -0
- package/dist-server/service/data-entry/entry-screen.js.map +1 -0
- package/dist-server/service/data-entry/entry-sources.js +6 -1
- package/dist-server/service/data-entry/entry-sources.js.map +1 -1
- package/dist-server/service/data-entry/entry-target.d.ts +6 -0
- package/dist-server/service/data-entry/entry-target.js.map +1 -1
- package/dist-server/service/data-entry/entry-tools.d.ts +3 -0
- package/dist-server/service/data-entry/entry-tools.js +3 -0
- package/dist-server/service/data-entry/entry-tools.js.map +1 -1
- package/dist-server/service/data-entry/entry-update.d.ts +3 -0
- package/dist-server/service/data-entry/entry-update.js +3 -0
- package/dist-server/service/data-entry/entry-update.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/server/service/assistant-mode/index.ts +2 -0
- package/server/service/assistant-mode/record-subject.test.ts +41 -0
- package/server/service/assistant-mode/record-subject.ts +44 -0
- package/server/service/chat-session/attachment-tool-result.test.ts +30 -1
- package/server/service/chat-session/attachment-tool-result.ts +12 -0
- package/server/service/chat-session/attachment-tools.ts +5 -3
- package/server/service/data-entry/data-entry.test.ts +18 -0
- package/server/service/data-entry/entry-screen.ts +14 -0
- package/server/service/data-entry/entry-sources.ts +5 -1
- package/server/service/data-entry/entry-target.ts +6 -0
- package/server/service/data-entry/entry-tools.ts +3 -0
- package/server/service/data-entry/entry-update.ts +3 -0
- package/test/applied-screens.test.ts +65 -0
- package/test/assistant-dock.test.ts +6 -1
- package/test/entry-proposal.test.ts +46 -0
- package/translations/en.json +3 -0
- package/translations/es.json +3 -0
- package/translations/ja.json +3 -0
- package/translations/ko.json +3 -0
- package/translations/ms.json +3 -0
- package/translations/zh.json +3 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface AppliedScreensDeps {
|
|
2
|
+
/** The page on screen - its route name and the shell's page element (`PageView.refresh`, ADR-0174). */
|
|
3
|
+
page(): {
|
|
4
|
+
name?: string;
|
|
5
|
+
element?: {
|
|
6
|
+
refresh?: (options?: {
|
|
7
|
+
ask?: boolean;
|
|
8
|
+
}) => Promise<boolean>;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
/** The 360's refresh, asked whether it is about the shown subject. */
|
|
12
|
+
refresh360(about: (subject: {
|
|
13
|
+
kind: string;
|
|
14
|
+
id: string;
|
|
15
|
+
}) => boolean): boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Reads again what shows the records of an applied card. Never throws - a screen that cannot read keeps what it shows.
|
|
19
|
+
* `kept-edits` when the card's page was left unread for its unsaved edits - the person is told, or the list looks unapplied.
|
|
20
|
+
*/
|
|
21
|
+
export declare function refreshAppliedScreens(proposal: any, ids: string[] | undefined, deps: AppliedScreensDeps): Promise<'kept-edits' | undefined>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The screens that show what an applied card wrote read it again (2026-10-29). Applied from the dock, a card left the
|
|
3
|
+
* list and the 360 behind it as they were, and people took it for not applied.
|
|
4
|
+
*
|
|
5
|
+
* Only a screen that shows those records reads again - the shown 360 of the record or of a record the card is about,
|
|
6
|
+
* and the page that is the card's screen. Another page keeps its rows and its scroll. A page with unsaved edits is
|
|
7
|
+
* not asked about them by a refresh nobody pressed: it is left as it is, and its own refresh control reads it later.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Reads again what shows the records of an applied card. Never throws - a screen that cannot read keeps what it shows.
|
|
11
|
+
* `kept-edits` when the card's page was left unread for its unsaved edits - the person is told, or the list looks unapplied.
|
|
12
|
+
*/
|
|
13
|
+
export async function refreshAppliedScreens(proposal, ids, deps) {
|
|
14
|
+
const kind = String(proposal?.target ?? '').split('.')[1] ?? '';
|
|
15
|
+
const axes = Array.isArray(proposal?.axes) ? proposal.axes : [];
|
|
16
|
+
try {
|
|
17
|
+
deps.refresh360(subject => (subject.kind === kind && !!ids?.includes(subject.id)) || axes.includes(subject.kind));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
/* the 360 keeps what it shows; its refresh button reads it */
|
|
21
|
+
}
|
|
22
|
+
const page = deps.page();
|
|
23
|
+
if (!proposal?.screen || page.name !== proposal.screen || typeof page.element?.refresh !== 'function')
|
|
24
|
+
return;
|
|
25
|
+
try {
|
|
26
|
+
return (await page.element.refresh({ ask: false })) === false ? 'kept-edits' : undefined;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
/* the page keeps what it shows; its refresh control reads it */
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=applied-screens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"applied-screens.js","sourceRoot":"","sources":["../../client/utils/applied-screens.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AASH;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,QAAa,EAAE,GAAyB,EAAE,IAAwB;IAC5G,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;IAC/D,MAAM,IAAI,GAAa,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAA;IACzE,IAAI,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC,CAAC,GAAG,EAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;IACnH,CAAC;IAAC,MAAM,CAAC;QACP,8DAA8D;IAChE,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,CAAA;IACxB,IAAI,CAAC,QAAQ,EAAE,MAAM,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,MAAM,IAAI,OAAO,IAAI,CAAC,OAAO,EAAE,OAAO,KAAK,UAAU;QAAE,OAAM;IAC7G,IAAI,CAAC;QACH,OAAO,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,CAAA;IAC1F,CAAC;IAAC,MAAM,CAAC;QACP,gEAAgE;IAClE,CAAC;AACH,CAAC","sourcesContent":["/*\n * The screens that show what an applied card wrote read it again (2026-10-29). Applied from the dock, a card left the\n * list and the 360 behind it as they were, and people took it for not applied.\n *\n * Only a screen that shows those records reads again - the shown 360 of the record or of a record the card is about,\n * and the page that is the card's screen. Another page keeps its rows and its scroll. A page with unsaved edits is\n * not asked about them by a refresh nobody pressed: it is left as it is, and its own refresh control reads it later.\n */\n\nexport interface AppliedScreensDeps {\n /** The page on screen - its route name and the shell's page element (`PageView.refresh`, ADR-0174). */\n page(): { name?: string; element?: { refresh?: (options?: { ask?: boolean }) => Promise<boolean> } }\n /** The 360's refresh, asked whether it is about the shown subject. */\n refresh360(about: (subject: { kind: string; id: string }) => boolean): boolean\n}\n\n/**\n * Reads again what shows the records of an applied card. Never throws - a screen that cannot read keeps what it shows.\n * `kept-edits` when the card's page was left unread for its unsaved edits - the person is told, or the list looks unapplied.\n */\nexport async function refreshAppliedScreens(proposal: any, ids: string[] | undefined, deps: AppliedScreensDeps): Promise<'kept-edits' | undefined> {\n const kind = String(proposal?.target ?? '').split('.')[1] ?? ''\n const axes: string[] = Array.isArray(proposal?.axes) ? proposal.axes : []\n try {\n deps.refresh360(subject => (subject.kind === kind && !!ids?.includes(subject.id)) || axes.includes(subject.kind))\n } catch {\n /* the 360 keeps what it shows; its refresh button reads it */\n }\n const page = deps.page()\n if (!proposal?.screen || page.name !== proposal.screen || typeof page.element?.refresh !== 'function') return\n try {\n return (await page.element.refresh({ ask: false })) === false ? 'kept-edits' : undefined\n } catch {\n /* the page keeps what it shows; its refresh control reads it */\n }\n}\n"]}
|
|
@@ -7,6 +7,8 @@ export interface ProposalOutcome {
|
|
|
7
7
|
missed?: number;
|
|
8
8
|
/** A line for the conversation, besides the outcome itself (e.g. what was created). */
|
|
9
9
|
note?: string;
|
|
10
|
+
/** The ids of the records it wrote - the screens that show them read again. Not reported to the conversation. */
|
|
11
|
+
records?: string[];
|
|
10
12
|
}
|
|
11
13
|
export interface ProposalContext {
|
|
12
14
|
sessionId?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assistant-proposals.js","sourceRoot":"","sources":["../../client/utils/assistant-proposals.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"assistant-proposals.js","sourceRoot":"","sources":["../../client/utils/assistant-proposals.ts"],"names":[],"mappings":"AA8BA,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA2B,CAAA;AAEnD,mHAAmH;AACnH,MAAM,UAAU,uBAAuB,CAAC,IAAY,EAAE,OAAwB;IAC5E,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;AAC7B,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAwB;IACxD,OAAO,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;AAC9C,CAAC;AAeD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAwB,CAAA;AAE7C,MAAM,UAAU,oBAAoB,CAAC,IAAY,EAAE,IAAkB;IACnE,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;AACvB,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAwB;IACrD,OAAO,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;AAC3C,CAAC;AAED;;;GAGG;AACH,IAAI,YAAyC,CAAA;AAE7C,MAAM,UAAU,uBAAuB,CAAC,OAAwB;IAC9D,YAAY,GAAG,OAAO,CAAA;AACxB,CAAC;AAED,uGAAuG;AACvG,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,QAAa,EAAE,OAAwB;IACvE,MAAM,OAAO,GAAG,QAAQ,EAAE,IAAI,KAAK,OAAO,IAAI,YAAY,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,iBAAiB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IAC7G,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,uBAAuB,EAAE,CAAA;IACnF,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;QAChD,OAAO,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,YAAY,EAAE,CAAA;IACzF,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAG,KAAe,EAAE,OAAO,IAAI,QAAQ,EAAE,CAAA;IACjF,CAAC;AACH,CAAC","sourcesContent":["/*\n * Proposal handlers - each proposal kind is applied by the package that declared its tool\n * (design/ai/shared-dock.md §4). The dock has no kind-specific code: it finds the handler, runs it,\n * and reports the outcome on every branch (ADR-0070). A kind nobody handles is `unavailable`.\n */\nimport type { AssistantSurface } from './assistant-surface.js'\n\nexport type ProposalVerdict = 'accepted' | 'refused' | 'unavailable' | 'declined'\n\nexport interface ProposalOutcome {\n verdict: ProposalVerdict\n errorCode?: string\n applied?: number\n missed?: number\n /** A line for the conversation, besides the outcome itself (e.g. what was created). */\n note?: string\n /** The ids of the records it wrote - the screens that show them read again. Not reported to the conversation. */\n records?: string[]\n}\n\nexport interface ProposalContext {\n sessionId?: string\n surface?: AssistantSurface\n /** Asks the person before anything irreversible. `false` → the handler returns `declined`. */\n confirm(options: { title: string; text: string; confirmText?: string; cancelText?: string; type?: 'warning' | 'info' }): Promise<boolean>\n notify(level: 'info' | 'error', message: string): void\n}\n\nexport type ProposalHandler = (proposal: any, context: ProposalContext) => Promise<ProposalOutcome>\n\nconst handlers = new Map<string, ProposalHandler>()\n\n/** Registered by the package that declares the tool - `proposeAction` by twin, a figure candidate by figure-ui. */\nexport function registerProposalHandler(tool: string, handler: ProposalHandler): void {\n handlers.set(tool, handler)\n}\n\nexport function proposalHandlerOf(tool: string | undefined): ProposalHandler | undefined {\n return tool ? handlers.get(tool) : undefined\n}\n\n/**\n * How a package's card is drawn in the conversation, besides the framework's line: an element drawn under it (given\n * `proposal`, `sessionId` and `proposalKey`), and the words of its button. A dashboard card draws its preview as the\n * one looking at the conversation (ADR-0160 decision 6). Registered by the package that declares the tool.\n */\nexport interface ProposalView {\n /** A custom element's tag name - defined by the registering package. */\n previewTag?: string\n /** i18n key of the button, and its words when the key has none. */\n runLabelKey?: string\n runLabel?: string\n}\n\nconst views = new Map<string, ProposalView>()\n\nexport function registerProposalView(tool: string, view: ProposalView): void {\n views.set(tool, view)\n}\n\nexport function proposalViewOf(tool: string | undefined): ProposalView | undefined {\n return tool ? views.get(tool) : undefined\n}\n\n/*\n * Data entry cards are the framework's, whatever record they enter (design/ai/data-entry.md §5): one\n * handler asks the server to enter the stored rows. The dock sets it; no app registers one per record.\n */\nlet entryHandler: ProposalHandler | undefined\n\nexport function setEntryProposalHandler(handler: ProposalHandler): void {\n entryHandler = handler\n}\n\n/** Runs the proposal - never throws; a failure is a `refused` outcome with its message as the code. */\nexport async function runProposal(proposal: any, context: ProposalContext): Promise<ProposalOutcome> {\n const handler = proposal?.kind === 'entry' && entryHandler ? entryHandler : proposalHandlerOf(proposal?.tool)\n if (!handler) return { verdict: 'unavailable', errorCode: 'unknown-proposal-kind' }\n try {\n const outcome = await handler(proposal, context)\n return outcome?.verdict ? outcome : { verdict: 'unavailable', errorCode: 'no-outcome' }\n } catch (error) {\n return { verdict: 'refused', errorCode: (error as Error)?.message || 'failed' }\n }\n}\n"]}
|
|
@@ -39,5 +39,5 @@ export interface AssistantSelection {
|
|
|
39
39
|
export type SelectionTranslate = (key: string, defaultValue: string, options?: Record<string, unknown>) => string;
|
|
40
40
|
/** The note stored in the conversation - the person's line, then the key line. Empty when nothing was picked. */
|
|
41
41
|
export declare function selectionNote(selection: AssistantSelection, t: SelectionTranslate): string;
|
|
42
|
-
/** A note as the conversation draws it - without the
|
|
42
|
+
/** A note as the conversation draws it - without the lines only the model reads (a selection's keys, an entry's records). */
|
|
43
43
|
export declare function noteForDisplay(content: string): string;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* model is handed the note as stored. Until 2026-10-24 the screen sent the selection as JSON and the conversation
|
|
7
7
|
* opened on `{"source":"explicit-page-selection",…}`.
|
|
8
8
|
*/
|
|
9
|
+
import { ENTRY_RECORDS_PREFIX } from './entry-proposal.js';
|
|
9
10
|
/**
|
|
10
11
|
* The grid's own row picker - a list that hands its rows to the assistant shows it beside `ox-assistant-selection-bar`
|
|
11
12
|
* (data-grist gutter `row-selector`, many at once).
|
|
@@ -48,11 +49,11 @@ export function selectionNote(selection, t) {
|
|
|
48
49
|
const keys = `${SELECTION_KEYS_PREFIX} ${selection.list ? `${selection.list} ` : ''}${records.map(one => one.id).join(' ')}`;
|
|
49
50
|
return `${named}\n${keys}`;
|
|
50
51
|
}
|
|
51
|
-
/** A note as the conversation draws it - without the
|
|
52
|
+
/** A note as the conversation draws it - without the lines only the model reads (a selection's keys, an entry's records). */
|
|
52
53
|
export function noteForDisplay(content) {
|
|
53
54
|
return (content ?? '')
|
|
54
55
|
.split('\n')
|
|
55
|
-
.filter(line => !line.startsWith(SELECTION_KEYS_PREFIX))
|
|
56
|
+
.filter(line => !line.startsWith(SELECTION_KEYS_PREFIX) && !line.startsWith(ENTRY_RECORDS_PREFIX))
|
|
56
57
|
.join('\n')
|
|
57
58
|
.trim();
|
|
58
59
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assistant-selection.js","sourceRoot":"","sources":["../../client/utils/assistant-selection.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"assistant-selection.js","sourceRoot":"","sources":["../../client/utils/assistant-selection.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The records a person picked on a screen and handed to the assistant - `askAssistant({ selection })`.\n *\n * The conversation keeps them as a note: one line the person reads (「선택 1건 · 계약 대장: H2022019 · 브라이스톤 IT3.0\n * Website」) and one line the model reads - the list and the ids, after `[keys]`. The screen hides the key line; the\n * model is handed the note as stored. Until 2026-10-24 the screen sent the selection as JSON and the conversation\n * opened on `{\"source\":\"explicit-page-selection\",…}`.\n */\nimport { ENTRY_RECORDS_PREFIX } from './entry-proposal.js'\n\n/**\n * The grid's own row picker - a list that hands its rows to the assistant shows it beside `ox-assistant-selection-bar`\n * (data-grist gutter `row-selector`, many at once).\n */\nexport const SELECTION_ROW_PICKER = { type: 'gutter', gutterName: 'row-selector', multiple: true }\n\n/** A picked row as the person reads it, when the screen gives no words of its own - its code or number, then its name. */\nexport function selectionLabel(row: any): string {\n return [row?.code || row?.number, row?.name || row?.title || row?.description]\n .filter(Boolean)\n .join(' · ')\n}\n\n/**\n * The rows a person picked, as the assistant is handed them (`askAssistant({ selection })`): saved rows only (an id, no\n * pending edit), each once, in screen order - never whole rows. Given the list's axis, each id is the record's key\n * `<axis>:<uuid>`, the one that list's read tool takes (read-target.ts `recordIdOfKey`): a bare uuid names no tool, and\n * a model read a contract's id with a dashboard's card tool (2026-10-25, Office 미수금).\n */\nexport function selectedRecords(rows: any[], options: { axis?: string; labelOf?: (row: any) => string } = {}): { id: string; label: string }[] {\n const { axis, labelOf = selectionLabel } = options\n const seen = new Set<string>()\n return (rows ?? [])\n .filter(row => row?.id && !row.__dirty__ && !seen.has(row.id) && !!seen.add(row.id))\n .map(row => ({ id: axis ? `${axis}:${row.id}` : row.id, label: labelOf(row) || '' }))\n}\n\n/** The line the model reads begins with this; the conversation does not draw it. */\nexport const SELECTION_KEYS_PREFIX = '[keys]'\n\n/** Labels named on the person's line - the rest are counted. */\nexport const SELECTION_LABELS_SHOWN = 5\n\nexport interface AssistantSelection {\n /** The list the records were picked from, as the person knows it (「계약 대장」). */\n title: string\n /** The list's query (`contracts`) - which list the ids belong to, for the model. */\n list?: string\n records: { id: string; label?: string }[]\n}\n\nexport type SelectionTranslate = (key: string, defaultValue: string, options?: Record<string, unknown>) => string\n\n/** The note stored in the conversation - the person's line, then the key line. Empty when nothing was picked. */\nexport function selectionNote(selection: AssistantSelection, t: SelectionTranslate): string {\n const seen = new Set<string>()\n const records = (selection?.records ?? []).filter(one => one?.id && !seen.has(one.id) && !!seen.add(one.id))\n if (!records.length) return ''\n const labels = records.map(one => (one.label || '').replace(/\\s+/g, ' ').trim()).filter(Boolean)\n const shown = labels.slice(0, SELECTION_LABELS_SHOWN).join(', ')\n const rest = labels.length - Math.min(labels.length, SELECTION_LABELS_SHOWN)\n const line = t('selection-note', 'Selected {count} · {title}', { count: records.length, title: selection.title })\n const named = shown ? `${line}: ${shown}${rest > 0 ? ` ${t('selection-more', 'and {count} more', { count: rest })}` : ''}` : line\n const keys = `${SELECTION_KEYS_PREFIX} ${selection.list ? `${selection.list} ` : ''}${records.map(one => one.id).join(' ')}`\n return `${named}\\n${keys}`\n}\n\n/** A note as the conversation draws it - without the lines only the model reads (a selection's keys, an entry's records). */\nexport function noteForDisplay(content: string): string {\n return (content ?? '')\n .split('\\n')\n .filter(line => !line.startsWith(SELECTION_KEYS_PREFIX) && !line.startsWith(ENTRY_RECORDS_PREFIX))\n .join('\\n')\n .trim()\n}\n"]}
|
|
@@ -47,6 +47,9 @@ export interface EntryApplyAnswer extends ProposalOutcome {
|
|
|
47
47
|
};
|
|
48
48
|
/** Set by the handler from the card - which words the conversation line uses. */
|
|
49
49
|
op?: string;
|
|
50
|
+
/** Set by the handler from the card - which card and record kind the entered ids belong to. */
|
|
51
|
+
key?: string;
|
|
52
|
+
target?: string;
|
|
50
53
|
}
|
|
51
54
|
export declare function isEntryProposal(proposal: any): boolean;
|
|
52
55
|
export declare function entryReadyCount(proposal: any): number;
|
|
@@ -56,6 +59,25 @@ export declare function entryIssueKey(issue: EntryIssueView): string;
|
|
|
56
59
|
export declare function entryCellIssues(row: EntryRowView, field?: string): EntryIssueView[];
|
|
57
60
|
/** What stays in the conversation - the model reads it on its next turn, and the person sees what went in. */
|
|
58
61
|
export declare function entryOutcomeNote(answer: EntryApplyAnswer, t: (key: string, options?: any) => string): string;
|
|
62
|
+
/**
|
|
63
|
+
* The line of an entry note that names the records a card entered - for the card, which opens them on their screen, and
|
|
64
|
+
* for the model. The conversation does not draw it. Not `[keys]`: that line is the person's selection, and a card's
|
|
65
|
+
* records must not replace what "these" means.
|
|
66
|
+
*/
|
|
67
|
+
export declare const ENTRY_RECORDS_PREFIX = "[records]";
|
|
68
|
+
/** The card key, record kind and ids of an entry note's records line - undefined for any other content. */
|
|
69
|
+
export declare function entryRecordsOf(content: string): {
|
|
70
|
+
key: string;
|
|
71
|
+
target: string;
|
|
72
|
+
ids: string[];
|
|
73
|
+
} | undefined;
|
|
74
|
+
/** At most this many ids narrow a list in its address - more open the list as it is. */
|
|
75
|
+
export declare const ENTRY_SCREEN_IDS = 50;
|
|
76
|
+
/**
|
|
77
|
+
* Where the card's "see on the screen" goes. One record of a kind with a 360 opens that 360 beside its list; records
|
|
78
|
+
* of the list the screen shows narrow it to them, a condition the person sees and removes; otherwise the screen as it is.
|
|
79
|
+
*/
|
|
80
|
+
export declare function entryScreenAddress(proposal: any, ids: string[] | undefined, has360: (kind: string) => boolean): string;
|
|
59
81
|
/** The handler every entry card runs - built with the mutation so the rules are tested without a server. */
|
|
60
82
|
export declare function entryProposalHandler(apply: (sessionId: string, proposalKey: string, reviewedHash?: string) => Promise<EntryApplyAnswer>, t: (key: string, options?: any) => string, reviewedOf?: (key: string) => string | undefined, reportRefusal?: (notification: {
|
|
61
83
|
level: 'error';
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The data entry card's rules and its handler (operato-application/design/ai/data-entry.md §5).
|
|
3
|
+
*
|
|
4
|
+
* The card is a review, not an editor: a correction is said in the conversation and the model proposes
|
|
5
|
+
* again - the newer card replaces the older one. Pressing asks the server to enter the stored rows; the
|
|
6
|
+
* screen sends no rows of its own.
|
|
7
|
+
*/
|
|
8
|
+
import { encodeListQuery } from '@operato/utils/list-query.js';
|
|
1
9
|
import { reviewedHashOf, reviewStillHolds } from './entry-workspace.js';
|
|
2
10
|
export function isEntryProposal(proposal) {
|
|
3
11
|
return proposal?.kind === 'entry' && Array.isArray(proposal?.rows);
|
|
@@ -35,7 +43,51 @@ export function entryOutcomeNote(answer, t) {
|
|
|
35
43
|
: t('ai-assistant.entry.entered-none', { defaultValue: 'no row was entered.' });
|
|
36
44
|
const lines = refused.slice(0, 20).map(row => t('ai-assistant.entry.row-refused', { defaultValue: 'row {n}: {reason}', n: row.index + 1, reason: row.error || '' }));
|
|
37
45
|
const more = refused.length > 20 ? [t('ai-assistant.entry.more-refused', { defaultValue: '{count} more rows were not entered.', count: refused.length - 20 })] : [];
|
|
38
|
-
|
|
46
|
+
const records = entryRecordsLine(answer);
|
|
47
|
+
return [head, ...appliedNames(answer, t), ...lines, ...more, ...(records ? [records] : [])].join('\n');
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The line of an entry note that names the records a card entered - for the card, which opens them on their screen, and
|
|
51
|
+
* for the model. The conversation does not draw it. Not `[keys]`: that line is the person's selection, and a card's
|
|
52
|
+
* records must not replace what "these" means.
|
|
53
|
+
*/
|
|
54
|
+
export const ENTRY_RECORDS_PREFIX = '[records]';
|
|
55
|
+
const enteredIds = (answer) => [...new Set((answer.rows ?? []).filter(row => row.ok && row.id).map(row => row.id))];
|
|
56
|
+
function entryRecordsLine(answer) {
|
|
57
|
+
const ids = enteredIds(answer);
|
|
58
|
+
return ids.length && answer.key && answer.target ? `${ENTRY_RECORDS_PREFIX} ${answer.key} ${answer.target} ${ids.join(' ')}` : '';
|
|
59
|
+
}
|
|
60
|
+
/** The card key, record kind and ids of an entry note's records line - undefined for any other content. */
|
|
61
|
+
export function entryRecordsOf(content) {
|
|
62
|
+
const line = (content ?? '').split('\n').find(one => one.startsWith(`${ENTRY_RECORDS_PREFIX} `));
|
|
63
|
+
const [key, target, ...ids] = line ? line.slice(ENTRY_RECORDS_PREFIX.length).trim().split(/\s+/) : [];
|
|
64
|
+
return key && target && ids.length ? { key, target, ids } : undefined;
|
|
65
|
+
}
|
|
66
|
+
/** At most this many ids narrow a list in its address - more open the list as it is. */
|
|
67
|
+
export const ENTRY_SCREEN_IDS = 50;
|
|
68
|
+
/**
|
|
69
|
+
* Where the card's "see on the screen" goes. One record of a kind with a 360 opens that 360 beside its list; records
|
|
70
|
+
* of the list the screen shows narrow it to them, a condition the person sees and removes; otherwise the screen as it is.
|
|
71
|
+
*/
|
|
72
|
+
export function entryScreenAddress(proposal, ids, has360) {
|
|
73
|
+
const screen = proposal?.screen;
|
|
74
|
+
const kind = String(proposal?.target ?? '').split('.')[1] ?? '';
|
|
75
|
+
if (ids?.length === 1 && kind && has360(kind))
|
|
76
|
+
return `${screen}?${new URLSearchParams({ f: `${kind}:${ids[0]}` })}`;
|
|
77
|
+
if (ids?.length && ids.length <= ENTRY_SCREEN_IDS && proposal?.screenLists)
|
|
78
|
+
return `${screen}?${encodeListQuery({ filters: [{ name: 'id', operator: 'in', value: ids }] })}`;
|
|
79
|
+
return screen;
|
|
80
|
+
}
|
|
81
|
+
/*
|
|
82
|
+
* Which records went in, by name - the person finds them on the screen by it, and the model reads which ones they were.
|
|
83
|
+
* Lines of one record share its name; a record without one is only counted.
|
|
84
|
+
*/
|
|
85
|
+
function appliedNames(answer, t) {
|
|
86
|
+
const names = [...new Set((answer.rows ?? []).filter(row => row.ok && row.label).map(row => row.label))];
|
|
87
|
+
if (!names.length)
|
|
88
|
+
return [];
|
|
89
|
+
const shown = names.slice(0, 10).join(', ');
|
|
90
|
+
return [names.length > 10 ? `${shown} ${t('ai-assistant.entry.more-applied', { defaultValue: 'and {count} more', count: names.length - 10 })}` : shown];
|
|
39
91
|
}
|
|
40
92
|
/** The handler every entry card runs - built with the mutation so the rules are tested without a server. */
|
|
41
93
|
export function entryProposalHandler(apply, t, reviewedOf = reviewedHashOf, reportRefusal = async (notification) => {
|
|
@@ -50,7 +102,7 @@ export function entryProposalHandler(apply, t, reviewedOf = reviewedHashOf, repo
|
|
|
50
102
|
const reviewed = reviewedOf(proposal.key);
|
|
51
103
|
if (!reviewStillHolds(proposal, reviewed))
|
|
52
104
|
return { verdict: 'refused', errorCode: 'review-stale', note: t('ai-assistant.entry.review-stale', { defaultValue: 'this proposal changed after it was reviewed in the workspace - review it again before entering.' }) };
|
|
53
|
-
const answer = { ...(await apply(context.sessionId, proposal.key, reviewed)), op: proposal.op };
|
|
105
|
+
const answer = { ...(await apply(context.sessionId, proposal.key, reviewed)), op: proposal.op, key: proposal.key, target: proposal.target };
|
|
54
106
|
if (answer.verdict === 'refused' && answer.errorCode === 'door-closed' && answer.refusal) {
|
|
55
107
|
try {
|
|
56
108
|
await reportRefusal({ level: 'error', message: answer.refusal.message, ex: [answer.refusal] });
|
|
@@ -66,7 +118,8 @@ export function entryProposalHandler(apply, t, reviewedOf = reviewedHashOf, repo
|
|
|
66
118
|
...(answer.errorCode ? { errorCode: answer.errorCode } : {}),
|
|
67
119
|
applied: answer.applied ?? 0,
|
|
68
120
|
missed: answer.missed ?? 0,
|
|
69
|
-
note: entryOutcomeNote(answer, t)
|
|
121
|
+
note: entryOutcomeNote(answer, t),
|
|
122
|
+
records: enteredIds(answer)
|
|
70
123
|
};
|
|
71
124
|
};
|
|
72
125
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry-proposal.js","sourceRoot":"","sources":["../../client/utils/entry-proposal.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The data entry card's rules and its handler (operato-application/design/ai/data-entry.md §5).\n *\n * The card is a review, not an editor: a correction is said in the conversation and the model proposes\n * again - the newer card replaces the older one. Pressing asks the server to enter the stored rows; the\n * screen sends no rows of its own.\n */\nimport type { ProposalContext, ProposalOutcome } from './assistant-proposals.js'\nimport { reviewedHashOf, reviewStillHolds } from './entry-workspace.js'\n\nexport interface EntryIssueView {\n field?: string\n code: string\n messageKey?: string\n params?: Record<string, unknown>\n paramKeys?: Record<string, string>\n blocking: boolean\n}\n\nexport interface EntryRowView {\n index: number\n values: Record<string, unknown>\n sources: { attachmentId?: string; where?: string; person?: boolean }[]\n issues: EntryIssueView[]\n /** The server found this row's numbers and dates together on this line of the cited file. */\n evidence?: { attachmentId: string; line: number }\n}\n\nexport interface EntryTotalView {\n field: string\n all: number\n ready: number\n declared?: number\n declaredFound?: boolean\n difference?: number\n declaredBy?: 'model' | 'file'\n}\n\nexport interface EntryRowResult {\n index: number\n ok: boolean\n id?: string\n label?: string\n error?: string\n}\n\nexport interface EntryApplyAnswer extends ProposalOutcome {\n rows?: EntryRowResult[]\n refusal?: { message: string; extensions: Record<string, unknown>; path: string[] }\n /** Set by the handler from the card - which words the conversation line uses. */\n op?: string\n}\n\nexport function isEntryProposal(proposal: any): boolean {\n return proposal?.kind === 'entry' && Array.isArray(proposal?.rows)\n}\n\nexport function entryReadyCount(proposal: any): number {\n const rows: EntryRowView[] = proposal?.rows ?? []\n const blocked = (row: EntryRowView) => row.issues?.some(issue => issue.blocking)\n if (!proposal?.groupBy) return rows.filter(row => !blocked(row)).length\n /* Lines of one record go in together - a record with a line kept out is kept out whole, and its lines do not count. */\n const keyOf = (row: EntryRowView) => {\n const value: any = row.values?.[proposal.groupBy]\n return value === undefined || value === null || value === '' ? `row-${row.index}` : String(typeof value === 'object' ? value.id ?? value.text : value)\n }\n const out = new Set(rows.filter(blocked).map(keyOf))\n return rows.filter(row => !out.has(keyOf(row))).length\n}\n\n/** The i18n key of an issue - the target's own, or the framework's by code. */\nexport function entryIssueKey(issue: EntryIssueView): string {\n return issue.messageKey || `ai-assistant.entry.issue.${issue.code}`\n}\n\n/** The issues of one cell, and of the row as a whole (no field). */\nexport function entryCellIssues(row: EntryRowView, field?: string): EntryIssueView[] {\n return (row.issues ?? []).filter(issue => (field ? issue.field === field : !issue.field))\n}\n\n/** What stays in the conversation - the model reads it on its next turn, and the person sees what went in. */\nexport function entryOutcomeNote(answer: EntryApplyAnswer, t: (key: string, options?: any) => string): string {\n const refused = (answer.rows ?? []).filter(row => !row.ok)\n const head = answer.op === 'update'\n ? answer.applied\n ? t('ai-assistant.entry.changed', { defaultValue: 'changed {count} records.', count: answer.applied })\n : t('ai-assistant.entry.changed-none', { defaultValue: 'no record was changed.' })\n : answer.applied\n ? t('ai-assistant.entry.entered', { defaultValue: 'entered {count} rows.', count: answer.applied })\n : t('ai-assistant.entry.entered-none', { defaultValue: 'no row was entered.' })\n const lines = refused.slice(0, 20).map(row => t('ai-assistant.entry.row-refused', { defaultValue: 'row {n}: {reason}', n: row.index + 1, reason: row.error || '' }))\n const more = refused.length > 20 ? [t('ai-assistant.entry.more-refused', { defaultValue: '{count} more rows were not entered.', count: refused.length - 20 })] : []\n return [head, ...lines, ...more].join('\\n')\n}\n\n/** The handler every entry card runs - built with the mutation so the rules are tested without a server. */\nexport function entryProposalHandler(\n apply: (sessionId: string, proposalKey: string, reviewedHash?: string) => Promise<EntryApplyAnswer>,\n t: (key: string, options?: any) => string,\n reviewedOf: (key: string) => string | undefined = reviewedHashOf,\n reportRefusal: (notification: { level: 'error'; message: string; ex: NonNullable<EntryApplyAnswer['refusal']>[] }) => void | Promise<void> = async notification => {\n /* The generic toast transport. auth-ui alone interprets the refusal and offers an access request. */\n const { notify } = await import('@operato/layout')\n notify(notification)\n }\n) {\n return async (proposal: any, context: ProposalContext): Promise<ProposalOutcome> => {\n if (!context.sessionId || !proposal?.key) return { verdict: 'unavailable', errorCode: 'no-conversation' }\n /* Reviewed in the 작업 패널: entered only as it was shown there (design/ai/workspace.md §6-1) - here, and again at the server. */\n const reviewed = reviewedOf(proposal.key)\n if (!reviewStillHolds(proposal, reviewed)) return { verdict: 'refused', errorCode: 'review-stale', note: t('ai-assistant.entry.review-stale', { defaultValue: 'this proposal changed after it was reviewed in the workspace - review it again before entering.' }) } as any\n const answer = { ...(await apply(context.sessionId, proposal.key, reviewed)), op: proposal.op }\n if (answer.verdict === 'refused' && answer.errorCode === 'door-closed' && answer.refusal) {\n try {\n await reportRefusal({ level: 'error', message: answer.refusal.message, ex: [answer.refusal] })\n } catch {\n /* Notification delivery is best-effort; it never changes the stored proposal's refusal. */\n }\n }\n if (answer.errorCode === 'review-stale')\n return { verdict: 'refused', errorCode: 'review-stale', applied: 0, missed: 0, note: t('ai-assistant.entry.review-stale', { defaultValue: 'this proposal changed after it was reviewed in the workspace - review it again before entering.' }) }\n return {\n verdict: answer.verdict,\n ...(answer.errorCode ? { errorCode: answer.errorCode } : {}),\n applied: answer.applied ?? 0,\n missed: answer.missed ?? 0,\n note: entryOutcomeNote(answer, t)\n }\n }\n}\n\n/**\n * The newest card per record, over the conversation's proposals in order - an older one is replaced and\n * cannot be entered (the server refuses it as `superseded`; the card says so before anyone presses).\n */\nexport function latestEntryKeys(proposalsInOrder: any[][]): Map<string, string> {\n const latest = new Map<string, string>()\n for (const list of proposalsInOrder) for (const one of list ?? []) if (isEntryProposal(one) && one.key) latest.set(entryGroupOf(one), one.key)\n return latest\n}\n\n/** A 입력안 and a 수정안 of one record are counted apart - a newer one of the same kind replaces the older. */\nexport function entryGroupOf(proposal: any): string {\n return `${proposal?.target}:${proposal?.op ?? 'create'}`\n}\n\n/**\n * The card's title - the record's name from the target (`부서`, `거래처`) and the kind of card from here, so a\n * 수정안 is never titled as a 입력안.\n */\nexport function entryTitle(proposal: any, t: (key: string, options?: any) => string): string {\n const name = proposal?.labelKey ? t(proposal.labelKey) : proposal?.target ?? ''\n return proposal?.op === 'update'\n ? t('ai-assistant.entry.title-update', { defaultValue: '{name} to change', name })\n : t('ai-assistant.entry.title-create', { defaultValue: '{name} to enter', name })\n}\n\nexport function isChangeProposal(proposal: any): boolean {\n return isEntryProposal(proposal) && proposal.op === 'update'\n}\n"]}
|
|
1
|
+
{"version":3,"file":"entry-proposal.js","sourceRoot":"","sources":["../../client/utils/entry-proposal.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The data entry card's rules and its handler (operato-application/design/ai/data-entry.md §5).\n *\n * The card is a review, not an editor: a correction is said in the conversation and the model proposes\n * again - the newer card replaces the older one. Pressing asks the server to enter the stored rows; the\n * screen sends no rows of its own.\n */\nimport { encodeListQuery } from '@operato/utils/list-query.js'\n\nimport type { ProposalContext, ProposalOutcome } from './assistant-proposals.js'\nimport { reviewedHashOf, reviewStillHolds } from './entry-workspace.js'\n\nexport interface EntryIssueView {\n field?: string\n code: string\n messageKey?: string\n params?: Record<string, unknown>\n paramKeys?: Record<string, string>\n blocking: boolean\n}\n\nexport interface EntryRowView {\n index: number\n values: Record<string, unknown>\n sources: { attachmentId?: string; where?: string; person?: boolean }[]\n issues: EntryIssueView[]\n /** The server found this row's numbers and dates together on this line of the cited file. */\n evidence?: { attachmentId: string; line: number }\n}\n\nexport interface EntryTotalView {\n field: string\n all: number\n ready: number\n declared?: number\n declaredFound?: boolean\n difference?: number\n declaredBy?: 'model' | 'file'\n}\n\nexport interface EntryRowResult {\n index: number\n ok: boolean\n id?: string\n label?: string\n error?: string\n}\n\nexport interface EntryApplyAnswer extends ProposalOutcome {\n rows?: EntryRowResult[]\n refusal?: { message: string; extensions: Record<string, unknown>; path: string[] }\n /** Set by the handler from the card - which words the conversation line uses. */\n op?: string\n /** Set by the handler from the card - which card and record kind the entered ids belong to. */\n key?: string\n target?: string\n}\n\nexport function isEntryProposal(proposal: any): boolean {\n return proposal?.kind === 'entry' && Array.isArray(proposal?.rows)\n}\n\nexport function entryReadyCount(proposal: any): number {\n const rows: EntryRowView[] = proposal?.rows ?? []\n const blocked = (row: EntryRowView) => row.issues?.some(issue => issue.blocking)\n if (!proposal?.groupBy) return rows.filter(row => !blocked(row)).length\n /* Lines of one record go in together - a record with a line kept out is kept out whole, and its lines do not count. */\n const keyOf = (row: EntryRowView) => {\n const value: any = row.values?.[proposal.groupBy]\n return value === undefined || value === null || value === '' ? `row-${row.index}` : String(typeof value === 'object' ? value.id ?? value.text : value)\n }\n const out = new Set(rows.filter(blocked).map(keyOf))\n return rows.filter(row => !out.has(keyOf(row))).length\n}\n\n/** The i18n key of an issue - the target's own, or the framework's by code. */\nexport function entryIssueKey(issue: EntryIssueView): string {\n return issue.messageKey || `ai-assistant.entry.issue.${issue.code}`\n}\n\n/** The issues of one cell, and of the row as a whole (no field). */\nexport function entryCellIssues(row: EntryRowView, field?: string): EntryIssueView[] {\n return (row.issues ?? []).filter(issue => (field ? issue.field === field : !issue.field))\n}\n\n/** What stays in the conversation - the model reads it on its next turn, and the person sees what went in. */\nexport function entryOutcomeNote(answer: EntryApplyAnswer, t: (key: string, options?: any) => string): string {\n const refused = (answer.rows ?? []).filter(row => !row.ok)\n const head = answer.op === 'update'\n ? answer.applied\n ? t('ai-assistant.entry.changed', { defaultValue: 'changed {count} records.', count: answer.applied })\n : t('ai-assistant.entry.changed-none', { defaultValue: 'no record was changed.' })\n : answer.applied\n ? t('ai-assistant.entry.entered', { defaultValue: 'entered {count} rows.', count: answer.applied })\n : t('ai-assistant.entry.entered-none', { defaultValue: 'no row was entered.' })\n const lines = refused.slice(0, 20).map(row => t('ai-assistant.entry.row-refused', { defaultValue: 'row {n}: {reason}', n: row.index + 1, reason: row.error || '' }))\n const more = refused.length > 20 ? [t('ai-assistant.entry.more-refused', { defaultValue: '{count} more rows were not entered.', count: refused.length - 20 })] : []\n const records = entryRecordsLine(answer)\n return [head, ...appliedNames(answer, t), ...lines, ...more, ...(records ? [records] : [])].join('\\n')\n}\n\n/**\n * The line of an entry note that names the records a card entered - for the card, which opens them on their screen, and\n * for the model. The conversation does not draw it. Not `[keys]`: that line is the person's selection, and a card's\n * records must not replace what \"these\" means.\n */\nexport const ENTRY_RECORDS_PREFIX = '[records]'\n\nconst enteredIds = (answer: EntryApplyAnswer): string[] => [...new Set((answer.rows ?? []).filter(row => row.ok && row.id).map(row => row.id as string))]\n\nfunction entryRecordsLine(answer: EntryApplyAnswer): string {\n const ids = enteredIds(answer)\n return ids.length && answer.key && answer.target ? `${ENTRY_RECORDS_PREFIX} ${answer.key} ${answer.target} ${ids.join(' ')}` : ''\n}\n\n/** The card key, record kind and ids of an entry note's records line - undefined for any other content. */\nexport function entryRecordsOf(content: string): { key: string; target: string; ids: string[] } | undefined {\n const line = (content ?? '').split('\\n').find(one => one.startsWith(`${ENTRY_RECORDS_PREFIX} `))\n const [key, target, ...ids] = line ? line.slice(ENTRY_RECORDS_PREFIX.length).trim().split(/\\s+/) : []\n return key && target && ids.length ? { key, target, ids } : undefined\n}\n\n/** At most this many ids narrow a list in its address - more open the list as it is. */\nexport const ENTRY_SCREEN_IDS = 50\n\n/**\n * Where the card's \"see on the screen\" goes. One record of a kind with a 360 opens that 360 beside its list; records\n * of the list the screen shows narrow it to them, a condition the person sees and removes; otherwise the screen as it is.\n */\nexport function entryScreenAddress(proposal: any, ids: string[] | undefined, has360: (kind: string) => boolean): string {\n const screen: string = proposal?.screen\n const kind = String(proposal?.target ?? '').split('.')[1] ?? ''\n if (ids?.length === 1 && kind && has360(kind)) return `${screen}?${new URLSearchParams({ f: `${kind}:${ids[0]}` })}`\n if (ids?.length && ids.length <= ENTRY_SCREEN_IDS && proposal?.screenLists)\n return `${screen}?${encodeListQuery({ filters: [{ name: 'id', operator: 'in', value: ids }] })}`\n return screen\n}\n\n/*\n * Which records went in, by name - the person finds them on the screen by it, and the model reads which ones they were.\n * Lines of one record share its name; a record without one is only counted.\n */\nfunction appliedNames(answer: EntryApplyAnswer, t: (key: string, options?: any) => string): string[] {\n const names = [...new Set((answer.rows ?? []).filter(row => row.ok && row.label).map(row => row.label as string))]\n if (!names.length) return []\n const shown = names.slice(0, 10).join(', ')\n return [names.length > 10 ? `${shown} ${t('ai-assistant.entry.more-applied', { defaultValue: 'and {count} more', count: names.length - 10 })}` : shown]\n}\n\n/** The handler every entry card runs - built with the mutation so the rules are tested without a server. */\nexport function entryProposalHandler(\n apply: (sessionId: string, proposalKey: string, reviewedHash?: string) => Promise<EntryApplyAnswer>,\n t: (key: string, options?: any) => string,\n reviewedOf: (key: string) => string | undefined = reviewedHashOf,\n reportRefusal: (notification: { level: 'error'; message: string; ex: NonNullable<EntryApplyAnswer['refusal']>[] }) => void | Promise<void> = async notification => {\n /* The generic toast transport. auth-ui alone interprets the refusal and offers an access request. */\n const { notify } = await import('@operato/layout')\n notify(notification)\n }\n) {\n return async (proposal: any, context: ProposalContext): Promise<ProposalOutcome> => {\n if (!context.sessionId || !proposal?.key) return { verdict: 'unavailable', errorCode: 'no-conversation' }\n /* Reviewed in the 작업 패널: entered only as it was shown there (design/ai/workspace.md §6-1) - here, and again at the server. */\n const reviewed = reviewedOf(proposal.key)\n if (!reviewStillHolds(proposal, reviewed)) return { verdict: 'refused', errorCode: 'review-stale', note: t('ai-assistant.entry.review-stale', { defaultValue: 'this proposal changed after it was reviewed in the workspace - review it again before entering.' }) } as any\n const answer = { ...(await apply(context.sessionId, proposal.key, reviewed)), op: proposal.op, key: proposal.key, target: proposal.target }\n if (answer.verdict === 'refused' && answer.errorCode === 'door-closed' && answer.refusal) {\n try {\n await reportRefusal({ level: 'error', message: answer.refusal.message, ex: [answer.refusal] })\n } catch {\n /* Notification delivery is best-effort; it never changes the stored proposal's refusal. */\n }\n }\n if (answer.errorCode === 'review-stale')\n return { verdict: 'refused', errorCode: 'review-stale', applied: 0, missed: 0, note: t('ai-assistant.entry.review-stale', { defaultValue: 'this proposal changed after it was reviewed in the workspace - review it again before entering.' }) }\n return {\n verdict: answer.verdict,\n ...(answer.errorCode ? { errorCode: answer.errorCode } : {}),\n applied: answer.applied ?? 0,\n missed: answer.missed ?? 0,\n note: entryOutcomeNote(answer, t),\n records: enteredIds(answer)\n }\n }\n}\n\n/**\n * The newest card per record, over the conversation's proposals in order - an older one is replaced and\n * cannot be entered (the server refuses it as `superseded`; the card says so before anyone presses).\n */\nexport function latestEntryKeys(proposalsInOrder: any[][]): Map<string, string> {\n const latest = new Map<string, string>()\n for (const list of proposalsInOrder) for (const one of list ?? []) if (isEntryProposal(one) && one.key) latest.set(entryGroupOf(one), one.key)\n return latest\n}\n\n/** A 입력안 and a 수정안 of one record are counted apart - a newer one of the same kind replaces the older. */\nexport function entryGroupOf(proposal: any): string {\n return `${proposal?.target}:${proposal?.op ?? 'create'}`\n}\n\n/**\n * The card's title - the record's name from the target (`부서`, `거래처`) and the kind of card from here, so a\n * 수정안 is never titled as a 입력안.\n */\nexport function entryTitle(proposal: any, t: (key: string, options?: any) => string): string {\n const name = proposal?.labelKey ? t(proposal.labelKey) : proposal?.target ?? ''\n return proposal?.op === 'update'\n ? t('ai-assistant.entry.title-update', { defaultValue: '{name} to change', name })\n : t('ai-assistant.entry.title-create', { defaultValue: '{name} to enter', name })\n}\n\nexport function isChangeProposal(proposal: any): boolean {\n return isEntryProposal(proposal) && proposal.op === 'update'\n}\n"]}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
export { registerAssistantMode, getAssistantMode, listAssistantModes, assistantModeForSubject, getAssistantModeWarnings, clearAssistantModes } from './registry.js';
|
|
7
7
|
export { resolveModeSettings, assistantModeView } from './resolve.js';
|
|
8
8
|
export { registerConversationCategory, conversationCategoriesOf, turnSystemPrompt, type ConversationCategory } from './mode-categories.js';
|
|
9
|
+
export { recordSubjectDescriber, registerRecordSubject, type RecordSubject } from './record-subject.js';
|
|
9
10
|
export { ABSENCE_RULE, FINISH_RULE, NEXT_REQUEST_RULE, SELECTION_RULE, SPEAKER_RULE, conversationPrompt, isPersonRequest, splitNextRequest } from './screen-rules.js';
|
|
10
11
|
export type { AssistantModeSpec, AssistantModeSlash, AssistantModeWarning, UnattendedLimits } from './types.js';
|
|
11
12
|
export type { AssistantModeRef, ModeSettings, AssistantModeView, AssistantModeSlashView, Translate } from './resolve.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.splitNextRequest = exports.isPersonRequest = exports.conversationPrompt = exports.SPEAKER_RULE = exports.SELECTION_RULE = exports.NEXT_REQUEST_RULE = exports.FINISH_RULE = exports.ABSENCE_RULE = exports.turnSystemPrompt = exports.conversationCategoriesOf = exports.registerConversationCategory = exports.assistantModeView = exports.resolveModeSettings = exports.clearAssistantModes = exports.getAssistantModeWarnings = exports.assistantModeForSubject = exports.listAssistantModes = exports.getAssistantMode = exports.registerAssistantMode = void 0;
|
|
3
|
+
exports.splitNextRequest = exports.isPersonRequest = exports.conversationPrompt = exports.SPEAKER_RULE = exports.SELECTION_RULE = exports.NEXT_REQUEST_RULE = exports.FINISH_RULE = exports.ABSENCE_RULE = exports.registerRecordSubject = exports.recordSubjectDescriber = exports.turnSystemPrompt = exports.conversationCategoriesOf = exports.registerConversationCategory = exports.assistantModeView = exports.resolveModeSettings = exports.clearAssistantModes = exports.getAssistantModeWarnings = exports.assistantModeForSubject = exports.listAssistantModes = exports.getAssistantMode = exports.registerAssistantMode = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Modes — what each conversation is, declared by the app server that owns the screen.
|
|
6
6
|
*
|
|
@@ -21,6 +21,10 @@ var mode_categories_js_1 = require("./mode-categories.js");
|
|
|
21
21
|
Object.defineProperty(exports, "registerConversationCategory", { enumerable: true, get: function () { return mode_categories_js_1.registerConversationCategory; } });
|
|
22
22
|
Object.defineProperty(exports, "conversationCategoriesOf", { enumerable: true, get: function () { return mode_categories_js_1.conversationCategoriesOf; } });
|
|
23
23
|
Object.defineProperty(exports, "turnSystemPrompt", { enumerable: true, get: function () { return mode_categories_js_1.turnSystemPrompt; } });
|
|
24
|
+
/* The record an open 360 is about, named by the module that owns it (screen-pattern §2-1). */
|
|
25
|
+
var record_subject_js_1 = require("./record-subject.js");
|
|
26
|
+
Object.defineProperty(exports, "recordSubjectDescriber", { enumerable: true, get: function () { return record_subject_js_1.recordSubjectDescriber; } });
|
|
27
|
+
Object.defineProperty(exports, "registerRecordSubject", { enumerable: true, get: function () { return record_subject_js_1.registerRecordSubject; } });
|
|
24
28
|
var screen_rules_js_1 = require("./screen-rules.js");
|
|
25
29
|
Object.defineProperty(exports, "ABSENCE_RULE", { enumerable: true, get: function () { return screen_rules_js_1.ABSENCE_RULE; } });
|
|
26
30
|
Object.defineProperty(exports, "FINISH_RULE", { enumerable: true, get: function () { return screen_rules_js_1.FINISH_RULE; } });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/assistant-mode/index.ts"],"names":[],"mappings":";;;AAAA;;;;GAIG;AACH,6CAOsB;AANpB,oHAAA,qBAAqB,OAAA;AACrB,+GAAA,gBAAgB,OAAA;AAChB,iHAAA,kBAAkB,OAAA;AAClB,sHAAA,uBAAuB,OAAA;AACvB,uHAAA,wBAAwB,OAAA;AACxB,kHAAA,mBAAmB,OAAA;AAErB,2CAAqE;AAA5D,iHAAA,mBAAmB,OAAA;AAAE,+GAAA,iBAAiB,OAAA;AAC/C,mHAAmH;AACnH,2DAA0I;AAAjI,kIAAA,4BAA4B,OAAA;AAAE,8HAAA,wBAAwB,OAAA;AAAE,sHAAA,gBAAgB,OAAA;AACjF,qDAAqK;AAA5J,+GAAA,YAAY,OAAA;AAAE,8GAAA,WAAW,OAAA;AAAE,oHAAA,iBAAiB,OAAA;AAAE,iHAAA,cAAc,OAAA;AAAE,+GAAA,YAAY,OAAA;AAAE,qHAAA,kBAAkB,OAAA;AAAE,kHAAA,eAAe,OAAA;AAAE,mHAAA,gBAAgB,OAAA","sourcesContent":["/**\n * Modes — what each conversation is, declared by the app server that owns the screen.\n *\n * Design: ADR-0061 decision 7 (`operato-application/adr/ADR-0061.md`) and the AI standard's A2.\n */\nexport {\n registerAssistantMode,\n getAssistantMode,\n listAssistantModes,\n assistantModeForSubject,\n getAssistantModeWarnings,\n clearAssistantModes\n} from './registry.js'\nexport { resolveModeSettings, assistantModeView } from './resolve.js'\n/* A category another package opens in every conversation, in front on the pages it names (ADR-0160 decision 9). */\nexport { registerConversationCategory, conversationCategoriesOf, turnSystemPrompt, type ConversationCategory } from './mode-categories.js'\nexport { ABSENCE_RULE, FINISH_RULE, NEXT_REQUEST_RULE, SELECTION_RULE, SPEAKER_RULE, conversationPrompt, isPersonRequest, splitNextRequest } from './screen-rules.js'\nexport type { AssistantModeSpec, AssistantModeSlash, AssistantModeWarning, UnattendedLimits } from './types.js'\nexport type { AssistantModeRef, ModeSettings, AssistantModeView, AssistantModeSlashView, Translate } from './resolve.js'\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/assistant-mode/index.ts"],"names":[],"mappings":";;;AAAA;;;;GAIG;AACH,6CAOsB;AANpB,oHAAA,qBAAqB,OAAA;AACrB,+GAAA,gBAAgB,OAAA;AAChB,iHAAA,kBAAkB,OAAA;AAClB,sHAAA,uBAAuB,OAAA;AACvB,uHAAA,wBAAwB,OAAA;AACxB,kHAAA,mBAAmB,OAAA;AAErB,2CAAqE;AAA5D,iHAAA,mBAAmB,OAAA;AAAE,+GAAA,iBAAiB,OAAA;AAC/C,mHAAmH;AACnH,2DAA0I;AAAjI,kIAAA,4BAA4B,OAAA;AAAE,8HAAA,wBAAwB,OAAA;AAAE,sHAAA,gBAAgB,OAAA;AACjF,8FAA8F;AAC9F,yDAAuG;AAA9F,2HAAA,sBAAsB,OAAA;AAAE,0HAAA,qBAAqB,OAAA;AACtD,qDAAqK;AAA5J,+GAAA,YAAY,OAAA;AAAE,8GAAA,WAAW,OAAA;AAAE,oHAAA,iBAAiB,OAAA;AAAE,iHAAA,cAAc,OAAA;AAAE,+GAAA,YAAY,OAAA;AAAE,qHAAA,kBAAkB,OAAA;AAAE,kHAAA,eAAe,OAAA;AAAE,mHAAA,gBAAgB,OAAA","sourcesContent":["/**\n * Modes — what each conversation is, declared by the app server that owns the screen.\n *\n * Design: ADR-0061 decision 7 (`operato-application/adr/ADR-0061.md`) and the AI standard's A2.\n */\nexport {\n registerAssistantMode,\n getAssistantMode,\n listAssistantModes,\n assistantModeForSubject,\n getAssistantModeWarnings,\n clearAssistantModes\n} from './registry.js'\nexport { resolveModeSettings, assistantModeView } from './resolve.js'\n/* A category another package opens in every conversation, in front on the pages it names (ADR-0160 decision 9). */\nexport { registerConversationCategory, conversationCategoriesOf, turnSystemPrompt, type ConversationCategory } from './mode-categories.js'\n/* The record an open 360 is about, named by the module that owns it (screen-pattern §2-1). */\nexport { recordSubjectDescriber, registerRecordSubject, type RecordSubject } from './record-subject.js'\nexport { ABSENCE_RULE, FINISH_RULE, NEXT_REQUEST_RULE, SELECTION_RULE, SPEAKER_RULE, conversationPrompt, isPersonRequest, splitNextRequest } from './screen-rules.js'\nexport type { AssistantModeSpec, AssistantModeSlash, AssistantModeWarning, UnattendedLimits } from './types.js'\nexport type { AssistantModeRef, ModeSettings, AssistantModeView, AssistantModeSlashView, Translate } from './resolve.js'\n"]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A record whose 360 is open, named for the AI each turn (operato-application design/ai/screen-pattern.md §2-1).
|
|
3
|
+
*
|
|
4
|
+
* A 360 kind with `assistantSubject` (@operato/layout) anchors its conversation to `subject` · `<kind>:<row uuid>`. The
|
|
5
|
+
* module that owns the record says here what to call it: the kind is the axis, and the door is the GraphQL field its
|
|
6
|
+
* 360 reads the record by (`Query.invoice`). The record is named only for someone that door lets through - asked of
|
|
7
|
+
* AUTH's field table as a tool's door is asked (`tool-access.ts`), so the declaration is the door's, never a copy.
|
|
8
|
+
* A row of another domain, or one that is gone, is not named.
|
|
9
|
+
*/
|
|
10
|
+
import { type AssistantSubjectDescriber } from '@things-factory/ai-client-base';
|
|
11
|
+
import { toolAccessFor } from '../tool-access.js';
|
|
12
|
+
export interface RecordSubject {
|
|
13
|
+
/** The 360 kind - the anchor's `<axis>:` part. */
|
|
14
|
+
axis: string;
|
|
15
|
+
/** `invoice`, `sales opportunity` - English, a word the model can use in a sentence. */
|
|
16
|
+
noun: string;
|
|
17
|
+
/** The root field the 360 reads the record by, `Query.<field>` - whoever may not pass it is told no name. */
|
|
18
|
+
door: string;
|
|
19
|
+
/** The row of this domain, `null` when there is none there. */
|
|
20
|
+
read(rowId: string, domainId: string): Promise<any | null>;
|
|
21
|
+
/** The name the 360 shows. */
|
|
22
|
+
nameOf(row: any): string;
|
|
23
|
+
}
|
|
24
|
+
export declare function recordSubjectDescriber(subject: RecordSubject, accessFor?: typeof toolAccessFor): AssistantSubjectDescriber;
|
|
25
|
+
export declare function registerRecordSubject(subject: RecordSubject): void;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.recordSubjectDescriber = recordSubjectDescriber;
|
|
4
|
+
exports.registerRecordSubject = registerRecordSubject;
|
|
5
|
+
/**
|
|
6
|
+
* A record whose 360 is open, named for the AI each turn (operato-application design/ai/screen-pattern.md §2-1).
|
|
7
|
+
*
|
|
8
|
+
* A 360 kind with `assistantSubject` (@operato/layout) anchors its conversation to `subject` · `<kind>:<row uuid>`. The
|
|
9
|
+
* module that owns the record says here what to call it: the kind is the axis, and the door is the GraphQL field its
|
|
10
|
+
* 360 reads the record by (`Query.invoice`). The record is named only for someone that door lets through - asked of
|
|
11
|
+
* AUTH's field table as a tool's door is asked (`tool-access.ts`), so the declaration is the door's, never a copy.
|
|
12
|
+
* A row of another domain, or one that is gone, is not named.
|
|
13
|
+
*/
|
|
14
|
+
const ai_client_base_1 = require("@things-factory/ai-client-base");
|
|
15
|
+
const tool_access_js_1 = require("../tool-access.js");
|
|
16
|
+
function recordSubjectDescriber(subject, accessFor = tool_access_js_1.toolAccessFor) {
|
|
17
|
+
return {
|
|
18
|
+
anchorType: 'subject',
|
|
19
|
+
axis: subject.axis,
|
|
20
|
+
async describe({ anchorId, domain, state }) {
|
|
21
|
+
const rowId = anchorId.slice(subject.axis.length + 1);
|
|
22
|
+
if (!anchorId.startsWith(`${subject.axis}:`) || !rowId)
|
|
23
|
+
return undefined;
|
|
24
|
+
if (!(await accessFor({ state })(subject.door)))
|
|
25
|
+
return undefined;
|
|
26
|
+
const row = await subject.read(rowId, domain.id);
|
|
27
|
+
const name = row ? subject.nameOf(row) : '';
|
|
28
|
+
return name ? { noun: subject.noun, name } : undefined;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function registerRecordSubject(subject) {
|
|
33
|
+
(0, ai_client_base_1.registerAssistantSubject)(recordSubjectDescriber(subject));
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=record-subject.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record-subject.js","sourceRoot":"","sources":["../../../server/service/assistant-mode/record-subject.ts"],"names":[],"mappings":";;AA0BA,wDAaC;AAED,sDAEC;AA3CD;;;;;;;;GAQG;AACH,mEAAyG;AAEzG,sDAAiD;AAejD,SAAgB,sBAAsB,CAAC,OAAsB,EAAE,SAAS,GAAG,8BAAa;IACtF,OAAO;QACL,UAAU,EAAE,SAAS;QACrB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,KAAK,CAAC,QAAQ,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE;YACxC,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;YACrD,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK;gBAAE,OAAO,SAAS,CAAA;YACxE,IAAI,CAAC,CAAC,MAAM,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;gBAAE,OAAO,SAAS,CAAA;YACjE,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,CAAC,CAAA;YAChD,MAAM,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;YAC3C,OAAO,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;QACxD,CAAC;KACF,CAAA;AACH,CAAC;AAED,SAAgB,qBAAqB,CAAC,OAAsB;IAC1D,IAAA,yCAAwB,EAAC,sBAAsB,CAAC,OAAO,CAAC,CAAC,CAAA;AAC3D,CAAC","sourcesContent":["/**\n * A record whose 360 is open, named for the AI each turn (operato-application design/ai/screen-pattern.md §2-1).\n *\n * A 360 kind with `assistantSubject` (@operato/layout) anchors its conversation to `subject` · `<kind>:<row uuid>`. The\n * module that owns the record says here what to call it: the kind is the axis, and the door is the GraphQL field its\n * 360 reads the record by (`Query.invoice`). The record is named only for someone that door lets through - asked of\n * AUTH's field table as a tool's door is asked (`tool-access.ts`), so the declaration is the door's, never a copy.\n * A row of another domain, or one that is gone, is not named.\n */\nimport { registerAssistantSubject, type AssistantSubjectDescriber } from '@things-factory/ai-client-base'\n\nimport { toolAccessFor } from '../tool-access.js'\n\nexport interface RecordSubject {\n /** The 360 kind - the anchor's `<axis>:` part. */\n axis: string\n /** `invoice`, `sales opportunity` - English, a word the model can use in a sentence. */\n noun: string\n /** The root field the 360 reads the record by, `Query.<field>` - whoever may not pass it is told no name. */\n door: string\n /** The row of this domain, `null` when there is none there. */\n read(rowId: string, domainId: string): Promise<any | null>\n /** The name the 360 shows. */\n nameOf(row: any): string\n}\n\nexport function recordSubjectDescriber(subject: RecordSubject, accessFor = toolAccessFor): AssistantSubjectDescriber {\n return {\n anchorType: 'subject',\n axis: subject.axis,\n async describe({ anchorId, domain, state }) {\n const rowId = anchorId.slice(subject.axis.length + 1)\n if (!anchorId.startsWith(`${subject.axis}:`) || !rowId) return undefined\n if (!(await accessFor({ state })(subject.door))) return undefined\n const row = await subject.read(rowId, domain.id)\n const name = row ? subject.nameOf(row) : ''\n return name ? { noun: subject.noun, name } : undefined\n }\n }\n}\n\nexport function registerRecordSubject(subject: RecordSubject): void {\n registerAssistantSubject(recordSubjectDescriber(subject))\n}\n"]}
|
|
@@ -27,3 +27,4 @@ export declare function attachmentToolResult(attachmentId: string, file: FileFor
|
|
|
27
27
|
images?: undefined;
|
|
28
28
|
};
|
|
29
29
|
export declare const NO_SUCH_ATTACHMENT = "there is no file with this id in a conversation here - it is not an attachment. Do not ask for it again; a record is read with its own read tool.";
|
|
30
|
+
export declare function attachmentIdOf(text: unknown): string | undefined;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.NO_SUCH_ATTACHMENT = void 0;
|
|
4
4
|
exports.attachmentToolResult = attachmentToolResult;
|
|
5
|
+
exports.attachmentIdOf = attachmentIdOf;
|
|
5
6
|
function attachmentToolResult(attachmentId, file) {
|
|
6
7
|
return file.type === 'image'
|
|
7
8
|
? { attachmentId, name: file.name, type: 'image', images: [{ data: file.data, mediaType: file.mediaType }], note: 'the picture is attached right after this result - look at it.' }
|
|
@@ -12,4 +13,14 @@ function attachmentToolResult(attachmentId, file) {
|
|
|
12
13
|
* file eight times in one turn. The answer says the id is no file and where a record is read instead.
|
|
13
14
|
*/
|
|
14
15
|
exports.NO_SUCH_ATTACHMENT = 'there is no file with this id in a conversation here - it is not an attachment. Do not ask for it again; a record is read with its own read tool.';
|
|
16
|
+
/*
|
|
17
|
+
* The file's id in what the model wrote (2026-10-29, office.hatiolab.com): it passed the whole history line -
|
|
18
|
+
* 「<id>: 이력서.pdf.txt (text/plain, 1547 bytes) - readChatAttachment to open it」 - and postgres refused the text as a
|
|
19
|
+
* uuid (22P02). The id is the uuid the text begins with, after an optional "[attachment "; anything else - a record's
|
|
20
|
+
* key, a cut id - names no file and is not asked of the database.
|
|
21
|
+
*/
|
|
22
|
+
const LEADING_ID = /^\[?(?:attachment\s+)?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?![0-9a-f-])/i;
|
|
23
|
+
function attachmentIdOf(text) {
|
|
24
|
+
return typeof text === 'string' ? LEADING_ID.exec(text.trim())?.[1] : undefined;
|
|
25
|
+
}
|
|
15
26
|
//# sourceMappingURL=attachment-tool-result.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachment-tool-result.js","sourceRoot":"","sources":["../../../server/service/chat-session/attachment-tool-result.ts"],"names":[],"mappings":";;;AAaA,oDAIC;
|
|
1
|
+
{"version":3,"file":"attachment-tool-result.js","sourceRoot":"","sources":["../../../server/service/chat-session/attachment-tool-result.ts"],"names":[],"mappings":";;;AAaA,oDAIC;AAiBD,wCAEC;AAvBD,SAAgB,oBAAoB,CAAC,YAAoB,EAAE,IAAkB;IAC3E,OAAO,IAAI,CAAC,IAAI,KAAK,OAAO;QAC1B,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,+DAA+D,EAAE;QACnL,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,4CAA4C,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAA;AACzJ,CAAC;AAED;;;GAGG;AACU,QAAA,kBAAkB,GAC7B,mJAAmJ,CAAA;AAErJ;;;;;GAKG;AACH,MAAM,UAAU,GAAG,oGAAoG,CAAA;AAEvH,SAAgB,cAAc,CAAC,IAAa;IAC1C,OAAO,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;AACjF,CAAC","sourcesContent":["/*\n * What readChatAttachment answers for a file of the conversation (design/ai/shared-dock.md §9-4).\n *\n * A text file comes back as its text. A picture comes back as the picture: the reserved `images` field is taken out\n * of the JSON by the loop (ai-client-base takeToolImages) and sent as an image part of the same turn - so a receipt\n * sent three turns ago can be read again without asking the person to send it again.\n *\n * Pure, so it is tested without storage or the shell.\n */\nexport type FileForModel =\n | { type: 'image'; data: Buffer; mediaType: string; name: string }\n | { type: 'text'; text: string; name: string; truncated: boolean }\n\nexport function attachmentToolResult(attachmentId: string, file: FileForModel) {\n return file.type === 'image'\n ? { attachmentId, name: file.name, type: 'image', images: [{ data: file.data, mediaType: file.mediaType }], note: 'the picture is attached right after this result - look at it.' }\n : { attachmentId, name: file.name, type: 'text', text: file.text, ...(file.truncated ? { note: 'the file is longer; this is its beginning.' } : {}) }\n}\n\n/*\n * What an id that names no file of the conversation gets (2026-10-24): given a deposit's id, the model asked for it as a\n * file eight times in one turn. The answer says the id is no file and where a record is read instead.\n */\nexport const NO_SUCH_ATTACHMENT =\n 'there is no file with this id in a conversation here - it is not an attachment. Do not ask for it again; a record is read with its own read tool.'\n\n/*\n * The file's id in what the model wrote (2026-10-29, office.hatiolab.com): it passed the whole history line -\n * 「<id>: 이력서.pdf.txt (text/plain, 1547 bytes) - readChatAttachment to open it」 - and postgres refused the text as a\n * uuid (22P02). The id is the uuid the text begins with, after an optional \"[attachment \"; anything else - a record's\n * key, a cut id - names no file and is not asked of the database.\n */\nconst LEADING_ID = /^\\[?(?:attachment\\s+)?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?![0-9a-f-])/i\n\nexport function attachmentIdOf(text: unknown): string | undefined {\n return typeof text === 'string' ? LEADING_ID.exec(text.trim())?.[1] : undefined\n}\n"]}
|
|
@@ -35,9 +35,12 @@ const readChatAttachment = {
|
|
|
35
35
|
},
|
|
36
36
|
builder: async (args, context) => {
|
|
37
37
|
const { domain, user } = context?.state ?? {};
|
|
38
|
-
const
|
|
39
|
-
if (!domain?.id || !
|
|
38
|
+
const given = typeof args?.attachmentId === 'string' ? args.attachmentId.trim() : '';
|
|
39
|
+
if (!domain?.id || !given)
|
|
40
40
|
return { absent: 'no-attachment', message: 'no attachment id was given.' };
|
|
41
|
+
const id = (0, attachment_tool_result_js_1.attachmentIdOf)(given);
|
|
42
|
+
if (!id)
|
|
43
|
+
return { absent: 'not-found', attachmentId: given, message: attachment_tool_result_js_1.NO_SUCH_ATTACHMENT };
|
|
41
44
|
const row = await (0, shell_1.getRepository)(attachment_base_1.Attachment).findOne({ where: { id, domain: { id: domain.id }, refType: session_attachments_js_1.CHAT_ATTACHMENT_REF } });
|
|
42
45
|
const session = row && (await (0, shell_1.getRepository)(chat_session_js_1.ChatSession).findOne({ where: { id: row.refBy, domain: { id: domain.id } } }));
|
|
43
46
|
if (!row || !session)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachment-tools.js","sourceRoot":"","sources":["../../../server/service/chat-session/attachment-tools.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;GASG;AACH,mEAAuG;AACvG,qEAA4D;AAC5D,iDAAqD;AAErD,uDAA+C;AAC/C,mEAAwD;AACxD,6FAAwF;AACxF,qEAAkF;AAClF,iEAAoD;AACpD,+DAAgE;AAChE,
|
|
1
|
+
{"version":3,"file":"attachment-tools.js","sourceRoot":"","sources":["../../../server/service/chat-session/attachment-tools.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * readChatAttachment - the model opens a file sent in an earlier turn (design/ai/shared-dock.md §9-4).\n *\n * A file reaches the model in full only in the turn it was sent; later turns carry one line naming it.\n * When the model needs it again it asks here - a text file comes back as its text, a picture as the picture itself. The caller must be able to read the conversation the file\n * belongs to - the same gate as reading its messages (ADR-0073 decision 1) - so a file id from elsewhere\n * opens nothing.\n *\n * The category is opened for every mode that declares an attachment policy (resolve.ts).\n */\nimport { registerToolCategory, type ToolCategory, type ToolSpec } from '@things-factory/ai-client-base'\nimport { Attachment } from '@things-factory/attachment-base'\nimport { getRepository } from '@things-factory/shell'\n\nimport { ChatSession } from './chat-session.js'\nimport { mayReadSession } from './session-visibility.js'\nimport { isSessionParticipant } from '../chat-session-participant/participant-lookup.js'\nimport { attachmentForModel, CHAT_ATTACHMENT_REF } from './session-attachments.js'\nimport { textCharsOf } from './attachment-policy.js'\nimport { getAssistantMode } from '../assistant-mode/registry.js'\nimport { attachmentIdOf, attachmentToolResult, NO_SUCH_ATTACHMENT } from './attachment-tool-result.js'\n\nexport const CHAT_ATTACHMENT_CATEGORY = 'chat-attachments'\n\nconst readChatAttachment: ToolSpec = {\n kind: 'read',\n name: 'readChatAttachment',\n doors: ['Query.chatMessages'],\n description:\n 'Open a file that was sent earlier in this conversation. History names such files as \"[attachment <id>: <name> ...]\"; ' +\n 'pass that id. An image comes back as the picture itself, a text file as its text (cut when long - the answer says so).',\n schema: {\n type: 'object',\n properties: { attachmentId: { type: 'string', description: 'The id from the \"[attachment <id>: ...]\" line.' } },\n required: ['attachmentId']\n },\n builder: async (args: any, context: any) => {\n const { domain, user } = context?.state ?? {}\n const given = typeof args?.attachmentId === 'string' ? args.attachmentId.trim() : ''\n if (!domain?.id || !given) return { absent: 'no-attachment', message: 'no attachment id was given.' }\n const id = attachmentIdOf(given)\n if (!id) return { absent: 'not-found', attachmentId: given, message: NO_SUCH_ATTACHMENT }\n\n const row: any = await getRepository(Attachment).findOne({ where: { id, domain: { id: domain.id }, refType: CHAT_ATTACHMENT_REF } as any })\n const session: any = row && (await getRepository(ChatSession).findOne({ where: { id: row.refBy, domain: { id: domain.id } } as any }))\n if (!row || !session) return { absent: 'not-found', attachmentId: id, message: NO_SUCH_ATTACHMENT }\n\n const participant = await isSessionParticipant(domain.id, session.id, user?.id)\n if (!mayReadSession(session.visibility, participant)) {\n /* Same answer as absent - saying \"it exists but is private\" would itself leak. */\n return { absent: 'not-found', attachmentId: id, message: NO_SUCH_ATTACHMENT }\n }\n\n const file = await attachmentForModel(row, textCharsOf(getAssistantMode(session.kind || '')?.attachments))\n return attachmentToolResult(id, file)\n }\n} as ToolSpec\n\nexport const chatAttachmentCategory: ToolCategory = {\n name: CHAT_ATTACHMENT_CATEGORY,\n guidance:\n '- A line \"[attachment <id>: <name> ...]\" names a file sent in an earlier turn. Call readChatAttachment with that id before answering about its contents; never guess what a file says.',\n specs: [readChatAttachment]\n}\n\nregisterToolCategory(chatAttachmentCategory)\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { EntryTarget } from './entry-target.js';
|
|
2
|
+
/**
|
|
3
|
+
* Where the records of a card are seen once they went in - the target's own screen, else the list screen of the read
|
|
4
|
+
* target with the same key. Carried on the card, so it is there after a refresh. `screenLists` says that screen lists
|
|
5
|
+
* these very records and takes conditions in its address, so the card may open it narrowed to them.
|
|
6
|
+
*/
|
|
7
|
+
export declare function entryScreenOf(target: EntryTarget): {
|
|
8
|
+
screen: string;
|
|
9
|
+
screenLists?: true;
|
|
10
|
+
} | {};
|