@things-factory/ai-client-base 10.1.25 → 10.1.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-server/service/agentic-loop/agentic-loop.js +22 -8
- package/dist-server/service/agentic-loop/agentic-loop.js.map +1 -1
- package/dist-server/service/assistant/abort-notice.d.ts +3 -1
- package/dist-server/service/assistant/abort-notice.js +20 -6
- package/dist-server/service/assistant/abort-notice.js.map +1 -1
- package/dist-server/service/assistant/assistant-chat.d.ts +6 -0
- package/dist-server/service/assistant/assistant-chat.js +1 -1
- package/dist-server/service/assistant/assistant-chat.js.map +1 -1
- package/dist-server/service/assistant/index.d.ts +1 -0
- package/dist-server/service/assistant/index.js +1 -0
- package/dist-server/service/assistant/index.js.map +1 -1
- package/dist-server/service/assistant/registry-assistant.d.ts +12 -10
- package/dist-server/service/assistant/registry-assistant.js +13 -10
- package/dist-server/service/assistant/registry-assistant.js.map +1 -1
- package/dist-server/service/assistant/tool-scope.d.ts +25 -0
- package/dist-server/service/assistant/tool-scope.js +71 -0
- package/dist-server/service/assistant/tool-scope.js.map +1 -0
- package/dist-server/service/capability.d.ts +17 -0
- package/dist-server/service/capability.js +49 -0
- package/dist-server/service/capability.js.map +1 -0
- package/dist-server/service/index.d.ts +2 -0
- package/dist-server/service/index.js +3 -0
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/testing/tool-call-context.d.ts +10 -0
- package/dist-server/service/testing/tool-call-context.js +13 -0
- package/dist-server/service/testing/tool-call-context.js.map +1 -0
- package/dist-server/service/tool-registry/index.d.ts +3 -3
- package/dist-server/service/tool-registry/index.js +2 -1
- package/dist-server/service/tool-registry/index.js.map +1 -1
- package/dist-server/service/tool-registry/registry.d.ts +23 -0
- package/dist-server/service/tool-registry/registry.js +58 -1
- package/dist-server/service/tool-registry/registry.js.map +1 -1
- package/dist-server/service/tool-registry/types.d.ts +72 -0
- package/dist-server/service/tool-registry/types.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/docs/grounded-analysis-contract.md +1 -1
- package/package.json +2 -2
- package/server/service/agentic-loop/agentic-loop.ts +21 -8
- package/server/service/agentic-loop/proposal-key.test.ts +52 -0
- package/server/service/analysis-contract.test.ts +0 -1
- package/server/service/assistant/abort-notice.ts +33 -11
- package/server/service/assistant/assistant-chat.test.ts +24 -2
- package/server/service/assistant/assistant-chat.ts +7 -2
- package/server/service/assistant/index.ts +1 -0
- package/server/service/assistant/registry-assistant.test.ts +131 -4
- package/server/service/assistant/registry-assistant.ts +33 -25
- package/server/service/assistant/tool-scope.ts +86 -0
- package/server/service/capability.test.ts +52 -0
- package/server/service/capability.ts +58 -0
- package/server/service/index.ts +3 -0
- package/server/service/testing/tool-call-context.ts +28 -0
- package/server/service/tool-registry/index.ts +7 -3
- package/server/service/tool-registry/registry.test.ts +91 -1
- package/server/service/tool-registry/registry.ts +77 -1
- package/server/service/tool-registry/types.ts +78 -3
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
getToolKind,
|
|
15
15
|
hasTool,
|
|
16
16
|
getCategorySpecs,
|
|
17
|
-
clearToolRegistry, getCategoryGuidance } from './registry'
|
|
17
|
+
clearToolRegistry, getCategoryGuidance, getToolRegistryWarnings } from './registry'
|
|
18
18
|
import type { ToolCategory, ToolSpec } from './types'
|
|
19
19
|
|
|
20
20
|
// 테스트 간 격리
|
|
@@ -34,6 +34,8 @@ function makeCategory(name: string, specs: Partial<ToolSpec>[]): ToolCategory {
|
|
|
34
34
|
description: 'desc',
|
|
35
35
|
schema: {},
|
|
36
36
|
builder: () => ({}),
|
|
37
|
+
/* A tool that changes something must name its door (A1). The fixture does, like guidance. */
|
|
38
|
+
...(s.kind && s.kind !== 'read' ? { privilege: { category: 'fixture', privilege: 'mutation' } } : {}),
|
|
37
39
|
...s
|
|
38
40
|
})) as ToolSpec[]
|
|
39
41
|
}
|
|
@@ -174,6 +176,7 @@ describe('registry — spec 보존', () => {
|
|
|
174
176
|
kind: 'write',
|
|
175
177
|
name: 't1',
|
|
176
178
|
description: 'main desc',
|
|
179
|
+
privilege: { category: 'fixture', privilege: 'mutation' },
|
|
177
180
|
themeNote: '⚠ note',
|
|
178
181
|
schema: { type: 'object' },
|
|
179
182
|
builder: () => null
|
|
@@ -258,3 +261,90 @@ describe('registry — 지침 없는 카테고리는 등록되지 않는다', ()
|
|
|
258
261
|
expect(getToolCategories()[0].guidance).toContain('fixture guidance')
|
|
259
262
|
})
|
|
260
263
|
})
|
|
264
|
+
|
|
265
|
+
describe('registry — a tool that changes something names its door (A1, stage one: surface)', () => {
|
|
266
|
+
test.each(['write', 'action', 'external'] as const)(
|
|
267
|
+
'★ a %s tool without a privilege registers, and the omission is recorded as a code',
|
|
268
|
+
kind => {
|
|
269
|
+
registerToolCategory({
|
|
270
|
+
name: 'no-door',
|
|
271
|
+
guidance: '- fixture guidance for no-door',
|
|
272
|
+
specs: [{ kind, name: 'changesThings', description: 'd', schema: {}, builder: () => null } as any]
|
|
273
|
+
})
|
|
274
|
+
expect(getToolKind('changesThings')).toBe(kind)
|
|
275
|
+
expect(getToolRegistryWarnings()).toEqual([
|
|
276
|
+
expect.objectContaining({
|
|
277
|
+
code: 'tool-privilege-undeclared',
|
|
278
|
+
params: { category: 'no-door', tool: 'changesThings', kind }
|
|
279
|
+
})
|
|
280
|
+
])
|
|
281
|
+
}
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
test('a privilege with no privilege name is recorded the same way', () => {
|
|
285
|
+
registerToolCategory({
|
|
286
|
+
name: 'half-door',
|
|
287
|
+
guidance: '- fixture guidance for half-door',
|
|
288
|
+
specs: [
|
|
289
|
+
{ kind: 'external', name: 'halfDoor', description: 'd', schema: {}, builder: () => null, privilege: { category: 'board' } } as any
|
|
290
|
+
]
|
|
291
|
+
})
|
|
292
|
+
expect(getToolRegistryWarnings().map(w => w.params.tool)).toEqual(['halfDoor'])
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
test('a declared tool leaves no warning', () => {
|
|
296
|
+
registerToolCategory(makeCategory('declared', [{ name: 'w1', kind: 'write' }]))
|
|
297
|
+
expect(getToolRegistryWarnings()).toEqual([])
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
/*
|
|
301
|
+
* 읽는 도구도 자기가 닿는 문을 말한다(ADR-0071 결정 2). 그전까지 업무 권한을 스스로 확인하는 것은
|
|
302
|
+
* plant 뿐이었고, twin · figure 의 읽는 도구는 테넌트만 봤다 — 대화 문 권한만 있으면 그 자료를 읽었다.
|
|
303
|
+
*/
|
|
304
|
+
test('★ a read tool that says nothing about its door is recorded too', () => {
|
|
305
|
+
registerToolCategory(makeCategory('reads', [{ name: 'r1', kind: 'read' }]))
|
|
306
|
+
|
|
307
|
+
expect(getToolRegistryWarnings()).toEqual([
|
|
308
|
+
expect.objectContaining({ code: 'tool-privilege-undeclared', params: { category: 'reads', tool: 'r1', kind: 'read' } })
|
|
309
|
+
])
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
test('★ `doors: []` is an answer — a tool that reaches no door has nothing to gate', () => {
|
|
313
|
+
registerToolCategory({
|
|
314
|
+
name: 'surface-only',
|
|
315
|
+
guidance: '- fixture guidance for surface-only',
|
|
316
|
+
specs: [
|
|
317
|
+
{ kind: 'read', name: 'getOpenScene', description: 'd', schema: {}, builder: () => null, doors: [] } as any
|
|
318
|
+
]
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
expect(getToolRegistryWarnings()).toEqual([])
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
test('★ a named door with no privilege of its own is its own fact, not an omission', () => {
|
|
325
|
+
registerToolCategory({
|
|
326
|
+
name: 'waiting',
|
|
327
|
+
guidance: '- fixture guidance for waiting',
|
|
328
|
+
specs: [
|
|
329
|
+
{ kind: 'read', name: 'twinNowTool', description: 'd', schema: {}, builder: () => null, doors: ['Query.twinNow'] } as any
|
|
330
|
+
]
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
expect(getToolRegistryWarnings()).toEqual([
|
|
334
|
+
expect.objectContaining({
|
|
335
|
+
code: 'tool-door-has-no-privilege',
|
|
336
|
+
params: { category: 'waiting', tool: 'twinNowTool', kind: 'read', doors: ['Query.twinNow'] }
|
|
337
|
+
})
|
|
338
|
+
])
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
test('re-registering a category replaces its warnings instead of piling them up', () => {
|
|
342
|
+
const undeclared = { kind: 'write', name: 'w9', description: 'd', schema: {}, builder: () => null } as any
|
|
343
|
+
registerToolCategory({ name: 'again', guidance: '- g', specs: [undeclared] })
|
|
344
|
+
registerToolCategory({ name: 'again', guidance: '- g', specs: [undeclared] })
|
|
345
|
+
expect(getToolRegistryWarnings()).toHaveLength(1)
|
|
346
|
+
|
|
347
|
+
registerToolCategory({ name: 'again', guidance: '- g', specs: [{ ...undeclared, privilege: { category: 'c', privilege: 'mutation' } }] })
|
|
348
|
+
expect(getToolRegistryWarnings()).toEqual([])
|
|
349
|
+
})
|
|
350
|
+
})
|
|
@@ -29,6 +29,30 @@ import type { ToolCategory, ToolSpec } from './types'
|
|
|
29
29
|
|
|
30
30
|
const categories: Map<string, ToolCategory> = new Map()
|
|
31
31
|
|
|
32
|
+
/** A registration that was accepted but is not yet what the contract requires. Code + params, not prose. */
|
|
33
|
+
export interface ToolRegistryWarning {
|
|
34
|
+
/**
|
|
35
|
+
* `tool-privilege-undeclared` — nothing said which door this tool reaches.
|
|
36
|
+
* `tool-door-has-no-privilege` — the door was named and it declares no privilege of its own yet
|
|
37
|
+
* (twin's queries, waiting on AUTH stage two). The tool cannot be stricter than its door, so
|
|
38
|
+
* this is a different fact from saying nothing, and the report keeps the two apart.
|
|
39
|
+
*/
|
|
40
|
+
code: 'tool-privilege-undeclared' | 'tool-door-has-no-privilege'
|
|
41
|
+
params: { category: string; tool: string; kind: ToolSpec['kind']; doors?: string[] }
|
|
42
|
+
/** For a person reading it; the code and params are what callers act on. */
|
|
43
|
+
message: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const warnings: ToolRegistryWarning[] = []
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* What registration accepted but the contract does not yet allow. Stage one of A1 records these
|
|
50
|
+
* instead of refusing; a host shows them at boot, and when the list is empty the rule becomes a refusal.
|
|
51
|
+
*/
|
|
52
|
+
export function getToolRegistryWarnings(): ReadonlyArray<ToolRegistryWarning> {
|
|
53
|
+
return warnings.slice()
|
|
54
|
+
}
|
|
55
|
+
|
|
32
56
|
/**
|
|
33
57
|
* 카테고리 등록. tool name 충돌 시 throw, 이름 중복 등록 시 덮어쓰기 + warn.
|
|
34
58
|
*/
|
|
@@ -55,10 +79,61 @@ export function registerToolCategory(category: ToolCategory): void {
|
|
|
55
79
|
)
|
|
56
80
|
}
|
|
57
81
|
|
|
82
|
+
/*
|
|
83
|
+
* A tool that changes something must say which privilege it goes through (A1, 2026-09-17).
|
|
84
|
+
*
|
|
85
|
+
* The conversation door checks one privilege — may this person talk to the assistant. Without a
|
|
86
|
+
* privilege on the tool, that single check opened every tool behind it: a user holding only
|
|
87
|
+
* `ai-assistant:mutation` could reach board-import's `materializeImportSession` and create a
|
|
88
|
+
* Board.
|
|
89
|
+
*
|
|
90
|
+
* **This is stage one of two: surface, then refuse** (ADR-0037, "먼저 드러내고, 목록이 비면 막는다").
|
|
91
|
+
* Refusing now would stop every app whose tools predate the `privilege` field from booting, and
|
|
92
|
+
* those apps compile against an installed copy that does not have the field yet. So an undeclared
|
|
93
|
+
* tool still registers, and the omission is recorded as a code (`getToolRegistryWarnings`), not
|
|
94
|
+
* a log line — a warning only a log carries is not distinguishable from no warning. When the list
|
|
95
|
+
* is empty across the apps, this becomes a refusal.
|
|
96
|
+
*/
|
|
97
|
+
warnings.splice(0, warnings.length, ...warnings.filter(w => w.params.category !== category.name))
|
|
98
|
+
for (const spec of category.specs) {
|
|
99
|
+
const declared = Boolean(spec.privilege?.category && spec.privilege?.privilege)
|
|
100
|
+
if (declared) continue
|
|
101
|
+
|
|
102
|
+
/*
|
|
103
|
+
* An empty list says this tool reaches no door — the ones that hand back what the surface sent
|
|
104
|
+
* are that. Nothing to gate, nothing to report.
|
|
105
|
+
*/
|
|
106
|
+
if (Array.isArray(spec.doors) && spec.doors.length === 0) continue
|
|
107
|
+
|
|
108
|
+
if (spec.doors?.length) {
|
|
109
|
+
/*
|
|
110
|
+
* The door is named and carries no privilege yet. The tool cannot be stricter than the door
|
|
111
|
+
* it reaches, so this is not an omission by whoever wrote the tool — it is the door's turn
|
|
112
|
+
* (ADR-0071 decision 2). Reported under its own code so the two are not read as one list.
|
|
113
|
+
*/
|
|
114
|
+
warnings.push({
|
|
115
|
+
code: 'tool-door-has-no-privilege',
|
|
116
|
+
params: { category: category.name, tool: spec.name, kind: spec.kind, doors: spec.doors },
|
|
117
|
+
message:
|
|
118
|
+
`tool '${spec.name}' reaches ${spec.doors.join(' + ')}, which declare no privilege — ` +
|
|
119
|
+
'the tool is as open as the doors it reaches until they declare one'
|
|
120
|
+
})
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
warnings.push({
|
|
125
|
+
code: 'tool-privilege-undeclared',
|
|
126
|
+
params: { category: category.name, tool: spec.name, kind: spec.kind },
|
|
127
|
+
message:
|
|
128
|
+
`tool '${spec.name}' in category '${category.name}' says nothing about the doors it reaches — ` +
|
|
129
|
+
"declare `doors` with the same { category, privilege } those doors use, or `doors: []` when it reaches none"
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
58
133
|
if (categories.has(category.name)) {
|
|
59
134
|
console.warn(
|
|
60
135
|
`[board-ai/tool-registry] category '${category.name}' already registered — overwriting. ` +
|
|
61
|
-
|
|
136
|
+
'(normal on a dev hot reload; in production it means the same category is registered twice)'
|
|
62
137
|
)
|
|
63
138
|
}
|
|
64
139
|
|
|
@@ -180,4 +255,5 @@ export function getCategorySpecs(categoryName: string): ReadonlyArray<ToolSpec>
|
|
|
180
255
|
*/
|
|
181
256
|
export function clearToolRegistry(): void {
|
|
182
257
|
categories.clear()
|
|
258
|
+
warnings.length = 0
|
|
183
259
|
}
|
|
@@ -27,6 +27,34 @@
|
|
|
27
27
|
// 도메인 type 으로 좁힌다. 사용자가 ToolSpec<TWrite, TAction> generic 까지 정밀화하면
|
|
28
28
|
// 좋겠지만, 11~50 개 spec 마다 type 인자 전파가 부담이라 unknown 으로 합의.
|
|
29
29
|
|
|
30
|
+
// ── ToolCallContext — what the neutral conversation door hands a registered tool (A3) ──
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* What `registryAssistantChat` — the door apps use (`assistantChat`, ADR-0061 decision 0) — hands a
|
|
34
|
+
* registered tool when it runs.
|
|
35
|
+
*
|
|
36
|
+
* `state` and `host` sit side by side. The twin's space tools read `ctx.state.host` while this door
|
|
37
|
+
* passed `ctx.host`, so they never saw the space; the two are never merged into one object.
|
|
38
|
+
*
|
|
39
|
+
* The board product's door (board-ai) passes its own shape and is not part of this contract.
|
|
40
|
+
*/
|
|
41
|
+
export interface ToolCallContext {
|
|
42
|
+
/** The request state the server built — domain, user, tx. The only basis for authorization. */
|
|
43
|
+
state: any
|
|
44
|
+
/**
|
|
45
|
+
* The client's `AssistantMode.hostContext`, passed through as is. **Untrusted** — a tool that reads
|
|
46
|
+
* an id from it checks that the id belongs to the caller's tenant. `{}` when none was sent.
|
|
47
|
+
*/
|
|
48
|
+
host: Record<string, any>
|
|
49
|
+
/** The conversation's mode. Filled once modes are registered on the server (A2); absent until then. */
|
|
50
|
+
mode?: ToolCallMode
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ToolCallMode {
|
|
54
|
+
kind: string
|
|
55
|
+
target?: string
|
|
56
|
+
}
|
|
57
|
+
|
|
30
58
|
// ── 카테고리별 builder 시그니처 ──────────────────────────────────────
|
|
31
59
|
|
|
32
60
|
/**
|
|
@@ -48,7 +76,6 @@ export interface ReadToolContext {
|
|
|
48
76
|
/** 이외 주입은 카테고리별로 자유. tool 등록자가 type 좁힐 수 있도록 unknown 으로 보존. */
|
|
49
77
|
[k: string]: any
|
|
50
78
|
}
|
|
51
|
-
|
|
52
79
|
/**
|
|
53
80
|
* Write tool — domain edit op 발생. host 가 patch 적용 후 영속화.
|
|
54
81
|
* 예: board-ai 의 addComponent / setFill / modifyBoard.
|
|
@@ -65,7 +92,6 @@ export interface WriteToolContext {
|
|
|
65
92
|
components?: ReadonlyArray<any>
|
|
66
93
|
[k: string]: any
|
|
67
94
|
}
|
|
68
|
-
|
|
69
95
|
/**
|
|
70
96
|
* Action tool — ephemeral scene 조작 (select / center / fit / sceneMode).
|
|
71
97
|
* board model 변경 X, undo 영향 X. host 가 별도 channel 로 적용.
|
|
@@ -91,9 +117,25 @@ export interface ExternalToolContext {
|
|
|
91
117
|
state?: any
|
|
92
118
|
[k: string]: any
|
|
93
119
|
}
|
|
94
|
-
|
|
95
120
|
// ── ToolSpec — 카테고리별 변형 ──────────────────────────────────────
|
|
96
121
|
|
|
122
|
+
/**
|
|
123
|
+
* The privilege a tool requires — the same `category`/`privilege` the tool's GraphQL door declares
|
|
124
|
+
* (`@privilege(category, privilege, owner?, super?)`), so the fields mirror `PrivilegeObject`.
|
|
125
|
+
*
|
|
126
|
+
* A tool is not a new door; it is another way into a door that already exists. A tool that creates
|
|
127
|
+
* a Board goes through the privilege that creating a Board already needs. Minting an AI-only
|
|
128
|
+
* privilege name would put two doors on one effect, and one of them would end up looser.
|
|
129
|
+
*/
|
|
130
|
+
export interface ToolPrivilege {
|
|
131
|
+
category: string
|
|
132
|
+
privilege: string
|
|
133
|
+
/** Domain owners pass without the privilege — mirror the door's `domainOwnerGranted`. */
|
|
134
|
+
owner?: boolean
|
|
135
|
+
/** Super users pass without the privilege — mirror the door's `superUserGranted`. */
|
|
136
|
+
super?: boolean
|
|
137
|
+
}
|
|
138
|
+
|
|
97
139
|
/**
|
|
98
140
|
* 모든 ToolSpec 공통 메타데이터.
|
|
99
141
|
*/
|
|
@@ -109,6 +151,39 @@ export interface ToolSpecBase {
|
|
|
109
151
|
themeNote?: string
|
|
110
152
|
/** JSON Schema for parameters — LLM tool schema 에 그대로 노출. */
|
|
111
153
|
schema: any
|
|
154
|
+
/**
|
|
155
|
+
* The privilege a caller needs before this tool is offered or run. See `ToolPrivilege`.
|
|
156
|
+
*
|
|
157
|
+
* Expected for `write`, `action` and `external` — registration records a
|
|
158
|
+
* `tool-privilege-undeclared` warning for one without it (A1 stage one).
|
|
159
|
+
*/
|
|
160
|
+
privilege?: ToolPrivilege
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Which GraphQL doors this tool reaches — `['Query.twinNow']`,
|
|
164
|
+
* `['Query.productionSummary', 'Query.pendingSummary']`.
|
|
165
|
+
*
|
|
166
|
+
* **A read tool names its doors too** (ADR-0071 decision 2, shaped by ADR-0072). Before that,
|
|
167
|
+
* only plant's tools asked whether the caller may read what they read; twin's and figure's
|
|
168
|
+
* checked the tenant and nothing else, so holding the conversation's own privilege was enough to
|
|
169
|
+
* reach their business data.
|
|
170
|
+
*
|
|
171
|
+
* **Always a list, even for one** — one shape to read. A tool that answers from several doors
|
|
172
|
+
* needs the caller to hold **all** of them (AND, the same reading as AUTH's `requiresAll`):
|
|
173
|
+
* plant's factory overview answers from two, and asking for only one of them would hand back
|
|
174
|
+
* half of what the caller may not see.
|
|
175
|
+
*
|
|
176
|
+
* Three states, and the boot report keeps them apart:
|
|
177
|
+
* - doors and a privilege — the gate checks it
|
|
178
|
+
* - `doors: []` — this tool reaches no door at all. The ones that hand back what the surface
|
|
179
|
+
* sent (`getOpenScene`, `getStationScreen`) are this: there is nothing behind them to gate.
|
|
180
|
+
* - doors with no privilege — the doors themselves declare none yet (twin's queries, waiting
|
|
181
|
+
* on AUTH stage two). A tool cannot be stricter than the door it reaches.
|
|
182
|
+
*
|
|
183
|
+
* The privilege is **the door's**, copied, and a test in each app compares the copy with the
|
|
184
|
+
* directive on that field. When the schema can be read at boot, the copy goes away.
|
|
185
|
+
*/
|
|
186
|
+
doors?: string[]
|
|
112
187
|
}
|
|
113
188
|
|
|
114
189
|
export interface ReadToolSpec extends ToolSpecBase {
|