@things-factory/ai-client-base 10.1.205 → 10.1.207
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.d.ts +5 -1
- package/dist-server/service/agentic-loop/agentic-loop.js +3 -2
- package/dist-server/service/agentic-loop/agentic-loop.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 +9 -0
- package/dist-server/service/assistant/registry-assistant.js +15 -9
- package/dist-server/service/assistant/registry-assistant.js.map +1 -1
- package/dist-server/service/assistant/tool-budget.d.ts +50 -0
- package/dist-server/service/assistant/tool-budget.js +122 -0
- package/dist-server/service/assistant/tool-budget.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/server/service/agentic-loop/agentic-loop.ts +9 -3
- package/server/service/assistant/index.ts +1 -0
- package/server/service/assistant/registry-assistant.ts +29 -14
- package/server/service/assistant/tool-budget.test.ts +217 -0
- package/server/service/assistant/tool-budget.ts +148 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ToolBudgetError = exports.TOOL_BUDGET_CATEGORY = exports.OPEN_TOOLS = exports.MAX_OFFERED_TOOLS = void 0;
|
|
4
|
+
exports.planToolOffer = planToolOffer;
|
|
5
|
+
exports.openableToolCatalog = openableToolCatalog;
|
|
6
|
+
exports.openToolsSpec = openToolsSpec;
|
|
7
|
+
exports.toolOfferState = toolOfferState;
|
|
8
|
+
/** Half the smallest provider limit known (OpenAI, 128). One constant - an application or a mode does not change it. */
|
|
9
|
+
exports.MAX_OFFERED_TOOLS = 64;
|
|
10
|
+
exports.OPEN_TOOLS = 'openTools';
|
|
11
|
+
/** The category the opening tool is recorded under - it is not registered; no mode names it. */
|
|
12
|
+
exports.TOOL_BUDGET_CATEGORY = 'tool-budget';
|
|
13
|
+
class ToolBudgetError extends Error {
|
|
14
|
+
constructor(front) {
|
|
15
|
+
super(`[tool-budget] the tools put in front (${front}) and openTools do not fit in ${exports.MAX_OFFERED_TOOLS} - narrow the mode's own categories`);
|
|
16
|
+
this.front = front;
|
|
17
|
+
this.code = 'tool-budget-exceeded';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.ToolBudgetError = ToolBudgetError;
|
|
21
|
+
/**
|
|
22
|
+
* What a turn starts with. The whole scope when it fits; otherwise the front categories (all of them, or it throws),
|
|
23
|
+
* the front tools while there is room, and openTools.
|
|
24
|
+
*/
|
|
25
|
+
function planToolOffer(scope, front = {}) {
|
|
26
|
+
if (scope.specs.length <= exports.MAX_OFFERED_TOOLS)
|
|
27
|
+
return { offered: [...scope.specs], openable: [] };
|
|
28
|
+
const room = exports.MAX_OFFERED_TOOLS - 1;
|
|
29
|
+
const picked = new Map();
|
|
30
|
+
for (const name of front.categories ?? []) {
|
|
31
|
+
for (const spec of scope.categories.find(category => category.name === name)?.specs ?? [])
|
|
32
|
+
picked.set(spec.name, spec);
|
|
33
|
+
}
|
|
34
|
+
if (picked.size > room)
|
|
35
|
+
throw new ToolBudgetError(picked.size);
|
|
36
|
+
const byName = new Map(scope.specs.map(spec => [spec.name, spec]));
|
|
37
|
+
for (const name of front.tools ?? []) {
|
|
38
|
+
if (picked.size >= room)
|
|
39
|
+
break;
|
|
40
|
+
const spec = byName.get(name);
|
|
41
|
+
if (spec)
|
|
42
|
+
picked.set(name, spec);
|
|
43
|
+
}
|
|
44
|
+
const offered = [...picked.values()];
|
|
45
|
+
return { offered, openable: scope.specs.filter(spec => !picked.has(spec.name)) };
|
|
46
|
+
}
|
|
47
|
+
const ONE_LINE = 160;
|
|
48
|
+
/** The first sentence of a description, on one line - what the list says of a tool. */
|
|
49
|
+
function oneLine(description) {
|
|
50
|
+
const flat = String(description ?? '').replace(/\s+/g, ' ').trim();
|
|
51
|
+
const sentence = /^(.+?[.!?])(\s|$)/.exec(flat)?.[1] ?? flat;
|
|
52
|
+
return sentence.length > ONE_LINE ? `${sentence.slice(0, ONE_LINE - 1)}…` : sentence;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The list of tools the model may open - one function builds it, wherever it goes. It goes in the prompt: OpenAI is
|
|
56
|
+
* reported to refuse a tool description over 1024 characters, and Office's list runs to some 150 lines.
|
|
57
|
+
*/
|
|
58
|
+
function openableToolCatalog(openable) {
|
|
59
|
+
if (openable.length === 0)
|
|
60
|
+
return '';
|
|
61
|
+
return [
|
|
62
|
+
`Tools you can open with ${exports.OPEN_TOOLS} (name: what it does). Open the ones the question needs, then call them:`,
|
|
63
|
+
...openable.map(spec => `- ${spec.name}: ${oneLine(spec.description)}`)
|
|
64
|
+
].join('\n');
|
|
65
|
+
}
|
|
66
|
+
/** The opening tool's definition. */
|
|
67
|
+
function openToolsSpec(open) {
|
|
68
|
+
return {
|
|
69
|
+
kind: 'read',
|
|
70
|
+
name: exports.OPEN_TOOLS,
|
|
71
|
+
description: `Open more tools for the rest of this answer. Pass the exact names from the list of tools you can open; they are offered from your next step on, with how to use them. At most ${exports.MAX_OFFERED_TOOLS} tools are open at once.`,
|
|
72
|
+
schema: {
|
|
73
|
+
type: 'object',
|
|
74
|
+
required: ['names'],
|
|
75
|
+
properties: { names: { type: 'array', items: { type: 'string' }, description: 'Names from the list of tools you can open.' } }
|
|
76
|
+
},
|
|
77
|
+
builder: (args) => open(Array.isArray(args?.names) ? args.names.map(String) : [])
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The offer of one turn, as it grows. `open` adds tools to what the next call carries; it never runs one.
|
|
82
|
+
*/
|
|
83
|
+
function toolOfferState(scope, front = {}) {
|
|
84
|
+
const plan = planToolOffer(scope, front);
|
|
85
|
+
const openable = new Map(plan.openable.map(spec => [spec.name, spec]));
|
|
86
|
+
const opening = openable.size > 0 ? openToolsSpec(names => open(names)) : undefined;
|
|
87
|
+
const offered = new Map([...plan.offered, ...(opening ? [opening] : [])].map(spec => [spec.name, spec]));
|
|
88
|
+
const categoryOf = new Map(scope.categories.flatMap(category => category.specs.map(spec => [spec.name, category])));
|
|
89
|
+
function open(names) {
|
|
90
|
+
const unknown = names.filter(name => !openable.has(name) && !offered.has(name));
|
|
91
|
+
if (unknown.length) {
|
|
92
|
+
return { rejected: true, error: 'tool-not-offered', names: unknown, message: `these are not in the list of tools you can open: ${unknown.join(', ')}. Use the exact names from the list.` };
|
|
93
|
+
}
|
|
94
|
+
const fresh = [...new Set(names)].filter(name => !offered.has(name));
|
|
95
|
+
const room = exports.MAX_OFFERED_TOOLS - offered.size;
|
|
96
|
+
if (fresh.length > room) {
|
|
97
|
+
return {
|
|
98
|
+
rejected: true,
|
|
99
|
+
error: 'tool-budget-exceeded',
|
|
100
|
+
room,
|
|
101
|
+
message: `only ${room} more tools fit in this answer. Open fewer - the ones this question needs - or answer from what you have read.`
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
for (const name of fresh)
|
|
105
|
+
offered.set(name, openable.get(name));
|
|
106
|
+
const guidance = [...new Set(fresh.map(name => categoryOf.get(name)?.guidance).filter(Boolean))];
|
|
107
|
+
return { opened: fresh, ...(guidance.length ? { guidance: guidance.join('\n') } : {}), message: 'these tools are open from your next step on. Call them as the question needs.' };
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
/** What a call carries now. */
|
|
111
|
+
offered: () => [...offered.values()],
|
|
112
|
+
isOffered: (name) => offered.has(name),
|
|
113
|
+
/** In the scope and still to be opened - a call to one is answered with how to open it. */
|
|
114
|
+
isOpenable: (name) => openable.has(name) && !offered.has(name),
|
|
115
|
+
/** In the scope and not offered now. */
|
|
116
|
+
openable: () => [...openable.keys()].filter(name => !offered.has(name)),
|
|
117
|
+
/** The guidance of the categories in front - opened ones bring theirs with the result. */
|
|
118
|
+
frontGuidance: () => [...new Set(plan.offered.map(spec => categoryOf.get(spec.name)?.guidance).filter(Boolean))],
|
|
119
|
+
catalog: () => openableToolCatalog(plan.openable)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=tool-budget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-budget.js","sourceRoot":"","sources":["../../../server/service/assistant/tool-budget.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * How many tools one model call carries (ADR-0061 decision 9, operato-application design/ai/tool-budget.md).\n *\n * A conversation used to send every tool its mode opened on every call. Office's conversation opened 165, and OpenAI\n * refuses more than 128 - every turn of a person who may read everything failed. The tools also went with their\n * guidance, some 140,000 characters a call.\n *\n * Now a call carries at most `MAX_OFFERED_TOOLS`. When the scope is larger, the caller names what goes in front (the\n * screen's records, the tools this conversation used); the rest is listed once in the prompt and the model opens what\n * it needs with `openTools` for the rest of the turn. Whether a person may use a tool is decided as before - the list\n * holds only the tools of their scope, and opening runs no tool.\n */\nimport type { ToolSpec } from '../tool-registry/types.js'\nimport type { ScopedCategory, ToolScope } from './tool-scope.js'\n\n/** Half the smallest provider limit known (OpenAI, 128). One constant - an application or a mode does not change it. */\nexport const MAX_OFFERED_TOOLS = 64\n\nexport const OPEN_TOOLS = 'openTools'\n\n/** The category the opening tool is recorded under - it is not registered; no mode names it. */\nexport const TOOL_BUDGET_CATEGORY = 'tool-budget'\n\n/** What the caller puts in front when the scope does not fit: whole categories first, then single tools while there is room. */\nexport interface ToolFront {\n categories?: string[]\n tools?: string[]\n}\n\nexport class ToolBudgetError extends Error {\n code = 'tool-budget-exceeded'\n constructor(public front: number) {\n super(`[tool-budget] the tools put in front (${front}) and openTools do not fit in ${MAX_OFFERED_TOOLS} - narrow the mode's own categories`)\n }\n}\n\nexport interface ToolOffer {\n /** What the first call carries, openTools last when anything is left to open. */\n offered: ToolSpec[]\n /** In the scope and not offered - what openTools may open. */\n openable: ToolSpec[]\n}\n\n/**\n * What a turn starts with. The whole scope when it fits; otherwise the front categories (all of them, or it throws),\n * the front tools while there is room, and openTools.\n */\nexport function planToolOffer(scope: ToolScope, front: ToolFront = {}): ToolOffer {\n if (scope.specs.length <= MAX_OFFERED_TOOLS) return { offered: [...scope.specs], openable: [] }\n\n const room = MAX_OFFERED_TOOLS - 1\n const picked = new Map<string, ToolSpec>()\n for (const name of front.categories ?? []) {\n for (const spec of scope.categories.find(category => category.name === name)?.specs ?? []) picked.set(spec.name, spec)\n }\n if (picked.size > room) throw new ToolBudgetError(picked.size)\n\n const byName = new Map(scope.specs.map(spec => [spec.name, spec]))\n for (const name of front.tools ?? []) {\n if (picked.size >= room) break\n const spec = byName.get(name)\n if (spec) picked.set(name, spec)\n }\n\n const offered = [...picked.values()]\n return { offered, openable: scope.specs.filter(spec => !picked.has(spec.name)) }\n}\n\nconst ONE_LINE = 160\n\n/** The first sentence of a description, on one line - what the list says of a tool. */\nfunction oneLine(description: string | undefined): string {\n const flat = String(description ?? '').replace(/\\s+/g, ' ').trim()\n const sentence = /^(.+?[.!?])(\\s|$)/.exec(flat)?.[1] ?? flat\n return sentence.length > ONE_LINE ? `${sentence.slice(0, ONE_LINE - 1)}…` : sentence\n}\n\n/**\n * The list of tools the model may open - one function builds it, wherever it goes. It goes in the prompt: OpenAI is\n * reported to refuse a tool description over 1024 characters, and Office's list runs to some 150 lines.\n */\nexport function openableToolCatalog(openable: ReadonlyArray<ToolSpec>): string {\n if (openable.length === 0) return ''\n return [\n `Tools you can open with ${OPEN_TOOLS} (name: what it does). Open the ones the question needs, then call them:`,\n ...openable.map(spec => `- ${spec.name}: ${oneLine(spec.description)}`)\n ].join('\\n')\n}\n\n/** The opening tool's definition. */\nexport function openToolsSpec(open: (names: string[]) => unknown): ToolSpec {\n return {\n kind: 'read',\n name: OPEN_TOOLS,\n description:\n `Open more tools for the rest of this answer. Pass the exact names from the list of tools you can open; they are offered from your next step on, with how to use them. At most ${MAX_OFFERED_TOOLS} tools are open at once.`,\n schema: {\n type: 'object',\n required: ['names'],\n properties: { names: { type: 'array', items: { type: 'string' }, description: 'Names from the list of tools you can open.' } }\n },\n builder: (args: any) => open(Array.isArray(args?.names) ? args.names.map(String) : [])\n } as ToolSpec\n}\n\n/**\n * The offer of one turn, as it grows. `open` adds tools to what the next call carries; it never runs one.\n */\nexport function toolOfferState(scope: ToolScope, front: ToolFront = {}) {\n const plan = planToolOffer(scope, front)\n const openable = new Map(plan.openable.map(spec => [spec.name, spec]))\n const opening = openable.size > 0 ? openToolsSpec(names => open(names)) : undefined\n const offered = new Map<string, ToolSpec>([...plan.offered, ...(opening ? [opening] : [])].map(spec => [spec.name, spec]))\n const categoryOf = new Map<string, ScopedCategory>(scope.categories.flatMap(category => category.specs.map(spec => [spec.name, category] as [string, ScopedCategory])))\n\n function open(names: string[]) {\n const unknown = names.filter(name => !openable.has(name) && !offered.has(name))\n if (unknown.length) {\n return { rejected: true, error: 'tool-not-offered', names: unknown, message: `these are not in the list of tools you can open: ${unknown.join(', ')}. Use the exact names from the list.` }\n }\n const fresh = [...new Set(names)].filter(name => !offered.has(name))\n const room = MAX_OFFERED_TOOLS - offered.size\n if (fresh.length > room) {\n return {\n rejected: true,\n error: 'tool-budget-exceeded',\n room,\n message: `only ${room} more tools fit in this answer. Open fewer - the ones this question needs - or answer from what you have read.`\n }\n }\n for (const name of fresh) offered.set(name, openable.get(name)!)\n const guidance = [...new Set(fresh.map(name => categoryOf.get(name)?.guidance).filter(Boolean))]\n return { opened: fresh, ...(guidance.length ? { guidance: guidance.join('\\n') } : {}), message: 'these tools are open from your next step on. Call them as the question needs.' }\n }\n\n return {\n /** What a call carries now. */\n offered: () => [...offered.values()],\n isOffered: (name: string) => offered.has(name),\n /** In the scope and still to be opened - a call to one is answered with how to open it. */\n isOpenable: (name: string) => openable.has(name) && !offered.has(name),\n /** In the scope and not offered now. */\n openable: () => [...openable.keys()].filter(name => !offered.has(name)),\n /** The guidance of the categories in front - opened ones bring theirs with the result. */\n frontGuidance: () => [...new Set(plan.offered.map(spec => categoryOf.get(spec.name)?.guidance).filter(Boolean) as string[])],\n catalog: () => openableToolCatalog(plan.openable)\n }\n}\n"]}
|