@things-factory/ai-assistant 10.1.206 → 10.1.208
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 +2 -1
- package/client/components/chat-input-builder.test.ts +5 -0
- package/client/components/chat-input-builder.ts +4 -1
- package/client/setup-assistant-dock.ts +1 -0
- package/dist-client/components/assistant-chat.js +2 -1
- package/dist-client/components/assistant-chat.js.map +1 -1
- package/dist-client/components/chat-input-builder.d.ts +2 -0
- package/dist-client/components/chat-input-builder.js +2 -1
- package/dist-client/components/chat-input-builder.js.map +1 -1
- package/dist-client/components/chat-input-builder.test.js +4 -0
- package/dist-client/components/chat-input-builder.test.js.map +1 -1
- package/dist-client/setup-assistant-dock.js +1 -0
- package/dist-client/setup-assistant-dock.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/assistant-chat-resolver.d.ts +1 -0
- package/dist-server/service/assistant-chat-resolver.js +20 -0
- package/dist-server/service/assistant-chat-resolver.js.map +1 -1
- package/dist-server/service/assistant-mode/index.d.ts +1 -1
- package/dist-server/service/assistant-mode/index.js +3 -1
- package/dist-server/service/assistant-mode/index.js.map +1 -1
- package/dist-server/service/assistant-mode/mode-categories.d.ts +29 -0
- package/dist-server/service/assistant-mode/mode-categories.js +37 -1
- package/dist-server/service/assistant-mode/mode-categories.js.map +1 -1
- package/dist-server/service/assistant-mode/registry.js +20 -0
- package/dist-server/service/assistant-mode/registry.js.map +1 -1
- package/dist-server/service/assistant-mode/screen-rules.d.ts +13 -0
- package/dist-server/service/assistant-mode/screen-rules.js +32 -5
- package/dist-server/service/assistant-mode/screen-rules.js.map +1 -1
- package/dist-server/service/assistant-mode/types.d.ts +6 -1
- package/dist-server/service/assistant-mode/types.js.map +1 -1
- package/dist-server/service/chat-message/used-tools.d.ts +11 -0
- package/dist-server/service/chat-message/used-tools.js +33 -0
- package/dist-server/service/chat-message/used-tools.js.map +1 -0
- package/dist-server/service/data-entry/entry-target.d.ts +13 -0
- package/dist-server/service/data-entry/entry-target.js +20 -5
- package/dist-server/service/data-entry/entry-target.js.map +1 -1
- package/dist-server/service/data-entry/entry-tools.js +2 -2
- package/dist-server/service/data-entry/entry-tools.js.map +1 -1
- package/dist-server/service/data-entry/entry-update.d.ts +2 -1
- package/dist-server/service/data-entry/entry-update.js +4 -2
- package/dist-server/service/data-entry/entry-update.js.map +1 -1
- package/dist-server/service/data-read/read-target.d.ts +2 -0
- package/dist-server/service/data-read/read-target.js +2 -2
- package/dist-server/service/data-read/read-target.js.map +1 -1
- package/dist-server/service/data-read/read-tools.js +1 -1
- package/dist-server/service/data-read/read-tools.js.map +1 -1
- package/dist-server/service/testing/mode-conformance.d.ts +4 -0
- package/dist-server/service/testing/mode-conformance.js +6 -0
- package/dist-server/service/testing/mode-conformance.js.map +1 -1
- package/dist-server/service/unattended-run/run-unattended.js +2 -0
- package/dist-server/service/unattended-run/run-unattended.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/server/explicit-graphql-types.test.ts +1 -1
- package/server/service/assistant-chat-resolver.ts +21 -1
- package/server/service/assistant-mode/index.ts +1 -1
- package/server/service/assistant-mode/mode-categories.ts +47 -1
- package/server/service/assistant-mode/mode-front.test.ts +65 -0
- package/server/service/assistant-mode/open-subject-door.test.ts +16 -0
- package/server/service/assistant-mode/registry.test.ts +56 -0
- package/server/service/assistant-mode/registry.ts +20 -2
- package/server/service/assistant-mode/resolve.test.ts +2 -1
- package/server/service/assistant-mode/screen-rules.test.ts +35 -3
- package/server/service/assistant-mode/screen-rules.ts +34 -4
- package/server/service/assistant-mode/types.ts +14 -2
- package/server/service/chat-message/fold-history-window.test.ts +1 -1
- package/server/service/chat-message/used-tools.test.ts +19 -0
- package/server/service/chat-message/used-tools.ts +25 -0
- package/server/service/data-entry/data-entry-update.test.ts +11 -0
- package/server/service/data-entry/data-entry.test.ts +65 -1
- package/server/service/data-entry/entry-target.ts +30 -5
- package/server/service/data-entry/entry-tools.ts +2 -2
- package/server/service/data-entry/entry-update.ts +5 -2
- package/server/service/data-read/read-target.ts +5 -3
- package/server/service/data-read/read-tools.ts +1 -1
- package/server/service/data-read/record-keys.test.ts +36 -0
- package/server/service/testing/mode-conformance.ts +8 -1
- package/server/service/unattended-run/run-unattended.ts +2 -0
- package/test/assistant-dock.test.ts +1 -1
- package/translations/ko.json +2 -2
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Data entry from material (design/ai/data-entry.md) - the rules a row passes, what the tool hands back,
|
|
3
3
|
* and what the server asks again before it writes.
|
|
4
4
|
*/
|
|
5
|
-
import { offsetAt, zonedDateTimes, clearEntryTargets, declareEntryTarget, entryDoorOf, entryToolName, entryTotals, lineHas, normalizeEntryRows, printedTotalOf, verifyRowsAgainstSources, writtenForms, type EntryTarget } from './entry-target'
|
|
5
|
+
import { readFieldValue, recordIdOfKey, resolveReferences, offsetAt, zonedDateTimes, clearEntryTargets, declareEntryTarget, entryDoorOf, entryToolName, entryTotals, lineHas, normalizeEntryRows, printedTotalOf, verifyRowsAgainstSources, writtenForms, type EntryTarget } from './entry-target'
|
|
6
6
|
import { entryCategoryOf, proposeEntries } from './entry-tools'
|
|
7
7
|
import { applyEntryProposal, entryProposalAnswered, findEntryProposal, type EntryApplyDeps } from './entry-apply'
|
|
8
8
|
import { modeToolCategories } from '../assistant-mode/resolve'
|
|
@@ -410,3 +410,67 @@ describe("a moment is the domain's wall time", () => {
|
|
|
410
410
|
expect(result.rows[0].values.startTime).toBe('2026-10-01T08:30:00+09:00')
|
|
411
411
|
})
|
|
412
412
|
})
|
|
413
|
+
|
|
414
|
+
/*
|
|
415
|
+
* 2026-10-23: an office link card took `tax-invoice:<uuid>` - the key the link evidence and the open record show - and
|
|
416
|
+
* looked it up as an approval number. A reference that names a record of an axis reads that axis's key as its id.
|
|
417
|
+
*/
|
|
418
|
+
describe('a reference names the record the model was shown', () => {
|
|
419
|
+
const ID = 'fe0e8503-ec5f-4da4-9c07-2d342c93d498'
|
|
420
|
+
const invoice = {
|
|
421
|
+
name: 'invoice',
|
|
422
|
+
type: 'reference' as const,
|
|
423
|
+
axis: 'tax-invoice',
|
|
424
|
+
reference: {
|
|
425
|
+
query: 'query ($text: String) { invoices(text: $text) { items { id approvalNo } } }',
|
|
426
|
+
variables: (text: string) => ({ text }),
|
|
427
|
+
recordsOf: (data: any, text: string) => (data?.invoices?.items ?? []).filter((one: any) => one.id === text || one.approvalNo === text)
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const asked: string[] = []
|
|
431
|
+
const run = async (_query: string, variables: any) => {
|
|
432
|
+
asked.push(variables.text)
|
|
433
|
+
return { data: { invoices: { items: [{ id: ID, approvalNo: '20260120-10260120-17151515' }] } } }
|
|
434
|
+
}
|
|
435
|
+
const resolved = async (text: string, field: any = invoice) => {
|
|
436
|
+
const [row] = await resolveReferences([field], [{ values: { invoice: { text } }, issues: [] as any[] }], run)
|
|
437
|
+
return row.issues.length ? row.issues[0].code : row.values.invoice.id
|
|
438
|
+
}
|
|
439
|
+
beforeEach(() => (asked.length = 0))
|
|
440
|
+
|
|
441
|
+
it('★ the key `<axis>:<uuid>`, the bare uuid and the record\'s own words all find it', async () => {
|
|
442
|
+
expect(await resolved(`tax-invoice:${ID}`)).toBe(ID)
|
|
443
|
+
expect(asked).toEqual([ID])
|
|
444
|
+
expect(await resolved(ID)).toBe(ID)
|
|
445
|
+
expect(await resolved('20260120-10260120-17151515')).toBe(ID)
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
it('★ the key of a record of another axis is not this record, whatever its uuid', async () => {
|
|
449
|
+
expect(await resolved(`cash-transaction:${ID}`)).toBe('reference-not-found')
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
it('a reference with no axis reads what was written, as before', async () => {
|
|
453
|
+
const { axis, ...plain } = invoice
|
|
454
|
+
expect(await resolved(`tax-invoice:${ID}`, plain)).toBe('reference-not-found')
|
|
455
|
+
expect(asked).toEqual([`tax-invoice:${ID}`])
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
it('★ a field that holds a record\'s id reads its key as the id - one axis or any of several', () => {
|
|
459
|
+
const question = { name: 'questionId', type: 'string' as const, axis: 'strategy-question' }
|
|
460
|
+
expect(readFieldValue(question, ` strategy-question:${ID} `)).toEqual({ read: ID })
|
|
461
|
+
expect(readFieldValue(question, `initiative:${ID}`)).toEqual({ read: `initiative:${ID}` })
|
|
462
|
+
const owner = { name: 'ownerId', type: 'string' as const, axis: ['strategy-question', 'initiative'] }
|
|
463
|
+
expect(readFieldValue(owner, `initiative:${ID}`)).toEqual({ read: ID })
|
|
464
|
+
expect(readFieldValue({ name: 'note', type: 'string' }, `initiative:${ID}`)).toEqual({ read: `initiative:${ID}` })
|
|
465
|
+
/* A model that hands a resolved reference the key, not the id. */
|
|
466
|
+
expect(readFieldValue(invoice, { id: `tax-invoice:${ID}` })).toEqual({ read: { id: ID } })
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
it('a key names an id only when it is one', () => {
|
|
470
|
+
expect(recordIdOfKey('contract', `contract:${ID}`)).toBe(ID)
|
|
471
|
+
expect(recordIdOfKey('contract', ID.toUpperCase())).toBe(ID.toUpperCase())
|
|
472
|
+
expect(recordIdOfKey('contract', 'contract:H2026012')).toBeUndefined()
|
|
473
|
+
expect(recordIdOfKey('contract', 'H2026012')).toBeUndefined()
|
|
474
|
+
expect(recordIdOfKey('contract', `contract-round:${ID}`)).toBeUndefined()
|
|
475
|
+
})
|
|
476
|
+
})
|
|
@@ -36,6 +36,14 @@ export interface EntryField {
|
|
|
36
36
|
* taken; none or several keep the row out and say so - the server never picks among several.
|
|
37
37
|
*/
|
|
38
38
|
reference?: EntryReference
|
|
39
|
+
/**
|
|
40
|
+
* The axis of the record the field names (`tax-invoice`, `contract`) - several when it may name a record of each - for a
|
|
41
|
+
* record the model is shown by its key: an open record's anchor and an application's link evidence write
|
|
42
|
+
* `<axis>:<id>`. The key is read as the id it names, so what the model was shown is what it may write (2026-10-23: a
|
|
43
|
+
* card took `tax-invoice:<uuid>` from the evidence, looked it up as an approval number and found nothing). A reference
|
|
44
|
+
* reads it before its query runs; a string field holding an id reads it as the id.
|
|
45
|
+
*/
|
|
46
|
+
axis?: string | readonly string[]
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
export interface EntryReference {
|
|
@@ -46,6 +54,22 @@ export interface EntryReference {
|
|
|
46
54
|
recordsOf(data: any, text: string): { id: string; label?: string }[]
|
|
47
55
|
}
|
|
48
56
|
|
|
57
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The id a record's key names - `<axis>:<uuid>` as an anchor writes it, or the uuid alone. Nothing for a word, or for
|
|
61
|
+
* the key of a record of another axis: that one is not this record, whatever its uuid.
|
|
62
|
+
*/
|
|
63
|
+
export function recordIdOfKey(axis: string | readonly string[], text: string): string | undefined {
|
|
64
|
+
const value = String(text ?? '').trim()
|
|
65
|
+
const prefix = (typeof axis === 'string' ? [axis] : axis).map(one => `${one}:`).find(one => value.startsWith(one))
|
|
66
|
+
const id = prefix ? value.slice(prefix.length) : value
|
|
67
|
+
return UUID.test(id) ? id : undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** What the model wrote for a field that names a record - the id, when it wrote the record's key. */
|
|
71
|
+
const shownId = (field: EntryField, text: string): string => (field.axis && recordIdOfKey(field.axis, text)) || text
|
|
72
|
+
|
|
49
73
|
/** A resolved reference, as a row keeps it. */
|
|
50
74
|
export interface ReferenceValue {
|
|
51
75
|
id: string
|
|
@@ -381,11 +405,11 @@ export function readFieldValue(field: EntryField, value: unknown): { read?: unkn
|
|
|
381
405
|
return field.values!.includes(String(value).trim()) ? { read: String(value).trim() } : { code: 'not-in-list' }
|
|
382
406
|
case 'reference':
|
|
383
407
|
/* Resolved already ({ id }), or a name to look up ({ text }) - resolveReferences settles the second. */
|
|
384
|
-
if (value && typeof value === 'object' && typeof (value as any).id === 'string') return { read: { id: (value as any).id, ...((value as any).label ? { label: String((value as any).label) } : {}) } }
|
|
408
|
+
if (value && typeof value === 'object' && typeof (value as any).id === 'string') return { read: { id: shownId(field, (value as any).id), ...((value as any).label ? { label: String((value as any).label) } : {}) } }
|
|
385
409
|
if (value && typeof value === 'object' && typeof (value as any).text === 'string') return { read: { text: (value as any).text } }
|
|
386
410
|
return typeof value === 'string' || typeof value === 'number' ? { read: { text: String(value).trim() } } : { code: 'not-a-reference' }
|
|
387
411
|
default:
|
|
388
|
-
return { read: String(value).trim() }
|
|
412
|
+
return { read: shownId(field, String(value).trim()) }
|
|
389
413
|
}
|
|
390
414
|
}
|
|
391
415
|
|
|
@@ -651,11 +675,12 @@ export async function resolveReferences<T extends { values: Record<string, unkno
|
|
|
651
675
|
for (const field of references) {
|
|
652
676
|
const value: any = row.values[field.name]
|
|
653
677
|
if (!value || typeof value !== 'object' || typeof value.text !== 'string') continue
|
|
654
|
-
const
|
|
678
|
+
const text = shownId(field, value.text)
|
|
679
|
+
const key = `${field.name}\u0000${text}`
|
|
655
680
|
if (!asked.has(key)) {
|
|
656
681
|
try {
|
|
657
|
-
const answer = await run(field.reference!.query, field.reference!.variables(
|
|
658
|
-
asked.set(key, answer.errors?.length ? new Error(answer.errors[0].message) : field.reference!.recordsOf(answer.data,
|
|
682
|
+
const answer = await run(field.reference!.query, field.reference!.variables(text))
|
|
683
|
+
asked.set(key, answer.errors?.length ? new Error(answer.errors[0].message) : field.reference!.recordsOf(answer.data, text) ?? [])
|
|
659
684
|
} catch (error: any) {
|
|
660
685
|
asked.set(key, error instanceof Error ? error : new Error(String(error)))
|
|
661
686
|
}
|
|
@@ -153,7 +153,7 @@ function changeSpecs(target: EntryTarget, run: (context: any) => GraphqlRunner):
|
|
|
153
153
|
doors: [...new Set([rootFieldOf(update.mutation, 'mutation'), readDoor, ...referenceDoorsOf(changeableFields(target))])],
|
|
154
154
|
description:
|
|
155
155
|
`Propose changes to existing records: ${target.description} ` +
|
|
156
|
-
'Name each record by ref: "current" (the one on screen), "selected" (every row selected on screen)
|
|
156
|
+
'Name each record by ref: "current" (the one on screen), "selected" (every row selected on screen), an id a find tool returned, or the record\'s key (`<kind>:<id>`) as you were shown it. ' +
|
|
157
157
|
'The server reads each record, and the card shows what it holds now and what it becomes; nothing changes until the person presses it.',
|
|
158
158
|
schema: {
|
|
159
159
|
type: 'object',
|
|
@@ -163,7 +163,7 @@ function changeSpecs(target: EntryTarget, run: (context: any) => GraphqlRunner):
|
|
|
163
163
|
items: {
|
|
164
164
|
type: 'object',
|
|
165
165
|
properties: {
|
|
166
|
-
ref: { type: 'string', description: '"current", "selected",
|
|
166
|
+
ref: { type: 'string', description: '"current", "selected", a record id from a find tool, or the record\'s key as shown - never a guessed id.' },
|
|
167
167
|
values: { type: 'object', description: 'Only the fields that change, with their new values.', properties: Object.fromEntries(changeableFields(target).map(field => [field.name, fieldSchema(field)])) }
|
|
168
168
|
},
|
|
169
169
|
required: ['ref', 'values']
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
changeToolName,
|
|
14
14
|
findToolName,
|
|
15
15
|
readFieldValue,
|
|
16
|
+
recordIdOfKey,
|
|
16
17
|
resolveReferences,
|
|
17
18
|
zonedDateTimes,
|
|
18
19
|
sameFieldValue,
|
|
@@ -52,7 +53,8 @@ export function changeableFields(target: EntryTarget): EntryField[] {
|
|
|
52
53
|
/**
|
|
53
54
|
* The records the changes name. `current` is the record on screen, `selected` every row selected there -
|
|
54
55
|
* both from the surface's host context, which is **not trusted**: an id from it is only a candidate, read
|
|
55
|
-
* through the application before anything is shown. Any other ref is an id, from the find tool
|
|
56
|
+
* through the application before anything is shown. Any other ref is an id, from the find tool, or the key the record
|
|
57
|
+
* was shown by.
|
|
56
58
|
*/
|
|
57
59
|
export function resolveChangeRefs(
|
|
58
60
|
targetKey: string,
|
|
@@ -74,7 +76,8 @@ export function resolveChangeRefs(
|
|
|
74
76
|
if (ids.length) for (const id of ids) picked.push({ id, via: 'selected', values })
|
|
75
77
|
else unresolved.push('selected')
|
|
76
78
|
} else if (ref) {
|
|
77
|
-
|
|
79
|
+
/* An open record's key - `contract-round:<id>` for `office.contract-round` - names the record of this target's axis. */
|
|
80
|
+
picked.push({ id: recordIdOfKey(targetKey.split('.').pop()!, ref) ?? ref, via: 'id', values })
|
|
78
81
|
} else {
|
|
79
82
|
unresolved.push('no-ref')
|
|
80
83
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Pure but for the GraphQL runner, which comes in as an argument - the rules are tested without a server.
|
|
11
11
|
*/
|
|
12
|
-
import { readFieldValue, rootFieldOf, type EntryField } from '../data-entry/entry-target.js'
|
|
12
|
+
import { readFieldValue, recordIdOfKey, rootFieldOf, type EntryField } from '../data-entry/entry-target.js'
|
|
13
13
|
import { encodeListQuery } from '@operato/utils/list-query.js'
|
|
14
14
|
import type { GraphqlRunner } from '../data-entry/entry-update.js'
|
|
15
15
|
import { parse } from 'graphql'
|
|
@@ -143,6 +143,8 @@ export interface DetailArg {
|
|
|
143
143
|
values?: string[]
|
|
144
144
|
description?: string
|
|
145
145
|
required?: boolean
|
|
146
|
+
/** A record's id: the axis (or axes) of the record, whose key `<axis>:<id>` the model may have been shown (EntryField.axis). */
|
|
147
|
+
axis?: string | readonly string[]
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
/**
|
|
@@ -200,8 +202,8 @@ export function detailReadOf(key: string | undefined): DetailRead | undefined {
|
|
|
200
202
|
export function readCriteria(target: ReadTarget, args: any): { criteria: Record<string, unknown>; refused: { name: string; code: string }[] } {
|
|
201
203
|
const criteria: Record<string, unknown> = {}
|
|
202
204
|
const refused: { name: string; code: string }[] = []
|
|
203
|
-
/* One record by its id - every target takes it (the id a list answer or a find tool gave). */
|
|
204
|
-
if (typeof args?.id === 'string' && args.id.trim()) criteria.id = args.id.trim()
|
|
205
|
+
/* One record by its id - every target takes it (the id a list answer or a find tool gave, or the key `<axis>:<id>` an open record is shown by, the axis being the key's last part). */
|
|
206
|
+
if (typeof args?.id === 'string' && args.id.trim()) criteria.id = recordIdOfKey(target.key.split('.').pop()!, args.id) ?? args.id.trim()
|
|
205
207
|
if (target.search && typeof args?.text === 'string' && args.text.trim()) criteria.text = args.text.trim()
|
|
206
208
|
for (const criterion of target.criteria) {
|
|
207
209
|
const value = args?.[criterion.name]
|
|
@@ -246,7 +246,7 @@ export async function queryDetail(target: DetailRead, args: any, run: GraphqlRun
|
|
|
246
246
|
if (arg.required) refused.push({ name: arg.name, code: 'required' })
|
|
247
247
|
continue
|
|
248
248
|
}
|
|
249
|
-
const { read, code } = readFieldValue({ name: arg.name, type: arg.type === 'string' ? 'text' : arg.type, values: arg.values } as any, value)
|
|
249
|
+
const { read, code } = readFieldValue({ name: arg.name, type: arg.type === 'string' ? 'text' : arg.type, values: arg.values, axis: arg.axis } as any, value)
|
|
250
250
|
if (read === undefined) refused.push({ name: arg.name, code: code ?? 'unreadable' })
|
|
251
251
|
else variables[arg.name] = read
|
|
252
252
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A read takes a record by the key it was shown by (2026-10-23) - `<axis>:<uuid>`, as an open record's anchor and an
|
|
3
|
+
* application's evidence write it. A 360 read and a list read asked by id took the key as the id and found nothing.
|
|
4
|
+
*/
|
|
5
|
+
import { readCriteria, type DetailRead, type ReadTarget } from './read-target'
|
|
6
|
+
import { queryDetail } from './read-tools'
|
|
7
|
+
|
|
8
|
+
const ID = 'f92c04ec-40c4-4be8-8160-080cff20e750'
|
|
9
|
+
|
|
10
|
+
describe('a read names the record the model was shown', () => {
|
|
11
|
+
const lines = { key: 'office.cash-transaction', criteria: [] } as unknown as ReadTarget
|
|
12
|
+
|
|
13
|
+
it('★ a list read by id takes the key of its own axis - the target key\'s last part', () => {
|
|
14
|
+
expect(readCriteria(lines, { id: `cash-transaction:${ID}` }).criteria).toEqual({ id: ID })
|
|
15
|
+
expect(readCriteria(lines, { id: ID }).criteria).toEqual({ id: ID })
|
|
16
|
+
/* Another record's key is not this record - asked as written, it finds nothing. */
|
|
17
|
+
expect(readCriteria(lines, { id: `contract:${ID}` }).criteria).toEqual({ id: `contract:${ID}` })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('★ a whole answer takes the key of the axis its id argument declares', async () => {
|
|
21
|
+
const summary: DetailRead = {
|
|
22
|
+
key: 'office.contract-360',
|
|
23
|
+
description: "one contract's 360.",
|
|
24
|
+
query: 'query ($id: String!) { contract360(id: $id) { code } }',
|
|
25
|
+
args: [{ name: 'id', type: 'string', required: true, axis: 'contract' }]
|
|
26
|
+
}
|
|
27
|
+
const asked: any[] = []
|
|
28
|
+
const run = async (_document: string, variables: any) => {
|
|
29
|
+
asked.push(variables)
|
|
30
|
+
return { data: { contract360: { code: 'H2026012' } } }
|
|
31
|
+
}
|
|
32
|
+
await queryDetail(summary, { id: `contract:${ID}` }, run)
|
|
33
|
+
await queryDetail(summary, { id: `cash-transaction:${ID}` }, run)
|
|
34
|
+
expect(asked).toEqual([{ id: ID }, { id: `cash-transaction:${ID}` }])
|
|
35
|
+
})
|
|
36
|
+
})
|
|
@@ -16,10 +16,13 @@ import { openSubjectLine, registryAssistantChat } from '@things-factory/ai-clien
|
|
|
16
16
|
|
|
17
17
|
import { getAssistantMode, getAssistantModeWarnings } from '../assistant-mode/registry.js'
|
|
18
18
|
import { assistantModeView, resolveModeSettings, type Translate } from '../assistant-mode/resolve.js'
|
|
19
|
+
import { modeFrontCategories, type TurnScreen } from '../assistant-mode/mode-categories.js'
|
|
19
20
|
|
|
20
21
|
export interface ModeRunResult {
|
|
21
22
|
/** 이 모드가 모델에게 실제로 건넨 도구 이름. */
|
|
22
23
|
offeredTools: string[]
|
|
24
|
+
/** 건네지 않았지만 모델이 openTools 로 꺼낼 수 있던 도구 — 둘을 합치면 이 모드로 쓸 수 있는 전부(ADR-0061 결정 9). */
|
|
25
|
+
openableTools: string[]
|
|
23
26
|
/** 모델이 받은 시스템 프롬프트 — 서버의 것인지 확인할 수 있다. */
|
|
24
27
|
systemPrompt: string
|
|
25
28
|
/** 첫 턴에 도구를 강제했는가. */
|
|
@@ -37,7 +40,7 @@ export const keyEchoTranslate: Translate = key => key
|
|
|
37
40
|
*/
|
|
38
41
|
export async function runRegisteredMode(
|
|
39
42
|
kind: string,
|
|
40
|
-
options: { client: any; hostContext?: any; state?: any; toolAccess?: any; reply?: string } = {} as any
|
|
43
|
+
options: { client: any; hostContext?: any; state?: any; toolAccess?: any; reply?: string; screen?: TurnScreen } = {} as any
|
|
41
44
|
): Promise<ModeRunResult> {
|
|
42
45
|
const settings = resolveModeSettings({ kind }, keyEchoTranslate)
|
|
43
46
|
let requiredToolOnFirstTurn = false
|
|
@@ -47,6 +50,8 @@ export async function runRegisteredMode(
|
|
|
47
50
|
messages: [{ role: 'user', content: '한 턴' }] as any,
|
|
48
51
|
systemPrompt: settings.systemPrompt,
|
|
49
52
|
toolCategories: settings.toolCategories,
|
|
53
|
+
/* What the door puts in front on that screen (ADR-0061 decision 9) - a mode too large for one call is offered as a person meets it. */
|
|
54
|
+
front: { categories: modeFrontCategories(getAssistantMode(kind) ?? {}, options.screen ?? {}) },
|
|
50
55
|
requireToolOnFirstTurn: (requiredToolOnFirstTurn = Boolean(settings.requireToolOnFirstTurn)),
|
|
51
56
|
temperature: settings.temperature,
|
|
52
57
|
maxTokens: settings.maxTokens,
|
|
@@ -65,6 +70,7 @@ export async function runRegisteredMode(
|
|
|
65
70
|
|
|
66
71
|
return {
|
|
67
72
|
offeredTools: [...result.offeredTools],
|
|
73
|
+
openableTools: [...result.openableTools],
|
|
68
74
|
systemPrompt: settings.systemPrompt,
|
|
69
75
|
requiredToolOnFirstTurn,
|
|
70
76
|
reply: result.reply
|
|
@@ -139,6 +145,7 @@ export function modeCarriesSharedRules(kind: string): string[] {
|
|
|
139
145
|
const found: string[] = []
|
|
140
146
|
if (/name our plumbing|do not describe our machinery/i.test(prompt)) found.push('absence-rule-copy')
|
|
141
147
|
if (/^\s*Next:/im.test(prompt) || /end the reply with one last line/i.test(prompt)) found.push('next-request-rule-copy')
|
|
148
|
+
if (/finish what was asked in this turn|never ask whether to propose/i.test(prompt)) found.push('finish-rule-copy')
|
|
142
149
|
return found
|
|
143
150
|
}
|
|
144
151
|
|
|
@@ -187,6 +187,8 @@ export async function runAssistantUnattended(input: UnattendedRunInput, deps: Un
|
|
|
187
187
|
messages,
|
|
188
188
|
systemPrompt,
|
|
189
189
|
toolCategories: categories,
|
|
190
|
+
/* No screen and no conversation - the mode's own tools in front, the lists opened as the run needs (ADR-0061 decision 9). */
|
|
191
|
+
front: { categories: mode.toolCategories ?? [] },
|
|
190
192
|
mode: { kind: mode.kind, ...(input.mode.target ? { target: input.mode.target } : {}) },
|
|
191
193
|
toolAccess: deps.toolAccess(state),
|
|
192
194
|
hostContext: input.hostContext,
|
|
@@ -144,7 +144,7 @@ describe('attachments in the chat follow the mode', () => {
|
|
|
144
144
|
/* Without it the client sends the File as {} and the server refuses the upload (found on :3700, 2026-09-25). */
|
|
145
145
|
assert.match(chat, /context: \{ hasUpload: true \}/)
|
|
146
146
|
/* Sent with the turn's input - the agent the person picked follows it (design/ai/agent-settings.md §5). */
|
|
147
|
-
assert.match(chat, /\n\s*attachmentIds,\s*\n\s*agentId: this\.agentId\s*\n\s*\}\)/)
|
|
147
|
+
assert.match(chat, /\n\s*attachmentIds,\s*\n\s*agentId: this\.agentId,\s*\n\s*page: \(store\.getState\(\) as any\)\?\.route\?\.page \|\| undefined\s*\n\s*\}\)/)
|
|
148
148
|
})
|
|
149
149
|
|
|
150
150
|
it('a stored file opens through the conversation route', () => {
|
package/translations/ko.json
CHANGED
|
@@ -260,7 +260,7 @@
|
|
|
260
260
|
"ai-assistant.entry.change": "{count}건 수정",
|
|
261
261
|
"ai-assistant.entry.title-create": "{name} 입력안",
|
|
262
262
|
"ai-assistant.entry.title-update": "{name} 수정안",
|
|
263
|
-
"ai-assistant.entry.changed": "{count}건을
|
|
263
|
+
"ai-assistant.entry.changed": "{count}건을 수정했습니다.",
|
|
264
264
|
"ai-assistant.entry.changed-none": "수정된 기록이 없습니다.",
|
|
265
265
|
"ai-assistant.entry.changes": "{count}건",
|
|
266
266
|
"ai-assistant.entry.not-changed": "{count}건은 변경하지 않습니다.",
|
|
@@ -399,5 +399,5 @@
|
|
|
399
399
|
"ai-assistant.run.assistant-run-model-error": "실행 중 AI 모델 호출이 실패했습니다.",
|
|
400
400
|
"ai-assistant.run.assistant-run-abandoned": "실행이 도중에 멈춰 다시 시작했습니다.",
|
|
401
401
|
"ai-assistant.run.ai-not-configured": "이 사업장에는 AI 가 설정되어 있지 않습니다.",
|
|
402
|
-
"ai-assistant.run.ai-agent-not-available": "이
|
|
402
|
+
"ai-assistant.run.ai-agent-not-available": "이 실행에서 선택한 AI 에이전트는 현재 사용할 수 없습니다."
|
|
403
403
|
}
|