@ultimat3/ai 2.0.0 → 4.0.0
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/CLAUDE.md +205 -4
- package/README.md +149 -4
- package/package.json +11 -9
- package/src/agent-facts.ts +70 -0
- package/src/agent-job.ts +97 -0
- package/src/agent-transcript.ts +94 -0
- package/src/agent.ts +170 -61
- package/src/errors.ts +2 -0
- package/src/fetch-seam.ts +15 -0
- package/src/gateway.ts +25 -11
- package/src/hive-errors.ts +30 -0
- package/src/hive-pool.ts +90 -0
- package/src/hive-result.ts +96 -0
- package/src/hive.ts +177 -0
- package/src/index.ts +15 -10
- package/src/llm-cache.ts +111 -0
- package/src/llm.ts +14 -66
- package/src/openai-provider.ts +21 -6
- package/src/openai-wire.ts +59 -25
- package/src/prompt.ts +6 -1
- package/src/provider.ts +34 -7
- package/src/rag.ts +66 -6
- package/src/remote-embedder.ts +3 -2
- package/src/runtime.ts +19 -5
- package/src/sse.ts +32 -1
- package/src/tools.ts +95 -8
- package/src/wire.ts +23 -13
package/src/tools.ts
CHANGED
|
@@ -8,13 +8,20 @@
|
|
|
8
8
|
// `run` below is `ProjectableAction`'s — the projection SEAM, which is what carries `invoke`.
|
|
9
9
|
// It is not a member of the action facade: an `action()` is `as`/`tool`/`openapi`/`job`/
|
|
10
10
|
// `contract` and the callable itself, and this header claimed `action.run` until 2026-08.
|
|
11
|
+
// `asProjectableAction` is what BUILDS that seam out of a real `action()`, so an app writes
|
|
12
|
+
// `agent({ tools: [publishPost] })` and never a hand-shaped stand-in — the same union
|
|
13
|
+
// @ultimat3/mcp's `ListedPrimitive` accepts, adapted at this package's own edge because the two
|
|
14
|
+
// wire formats want different schemas (issue #124).
|
|
11
15
|
//
|
|
12
16
|
// The JSON Schema type and the projectable-primitive shape are declared here rather than
|
|
13
17
|
// imported from @ultimat3/mcp: that package is the same tier, so importing it would be a
|
|
14
18
|
// boundary error. Both packages describe the same structural contract.
|
|
15
19
|
|
|
20
|
+
import type { AnyAction } from '@ultimat3/action';
|
|
21
|
+
import { actionName, invoke, isAction } from '@ultimat3/action';
|
|
16
22
|
import type { Actor } from '@ultimat3/core';
|
|
17
|
-
import { isMcpExposed } from '@ultimat3/core';
|
|
23
|
+
import { isMcpExposed, stringField } from '@ultimat3/core';
|
|
24
|
+
import { toMcpInputSchema } from '@ultimat3/schema';
|
|
18
25
|
|
|
19
26
|
/** The JSON Schema subset the framework emits for tool arguments. */
|
|
20
27
|
export interface JsonSchema {
|
|
@@ -68,6 +75,57 @@ export interface ProjectableAction {
|
|
|
68
75
|
run(args: { input: unknown; actor: Actor }): Promise<unknown>;
|
|
69
76
|
}
|
|
70
77
|
|
|
78
|
+
/**
|
|
79
|
+
* What `agent({ tools })` accepts: the real primitive an app writes, or a pre-projected one.
|
|
80
|
+
*
|
|
81
|
+
* The real `action()` comes first because it is what an app has. Until 2026-08 this list took
|
|
82
|
+
* `ProjectableAction` alone, which no `action()` structurally satisfies — an action carries
|
|
83
|
+
* `as`/`tool`/`openapi`/`job`/`contract` and never `run` — so the documented shape
|
|
84
|
+
* `agent({ tools: [publishPost] })` was a `TS2741` and every test in this package hand-built a
|
|
85
|
+
* stand-in, which is why the suite stayed green over an API that did not compile (issue #124).
|
|
86
|
+
* `ProjectableAction` stays in the union for a surface that builds its catalog programmatically
|
|
87
|
+
* and for a test that projects a fake.
|
|
88
|
+
*/
|
|
89
|
+
export type AgentTool = AnyAction | ProjectableAction;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Adapt whatever the author listed. The same shape @ultimat3/mcp's `asProjectable` produces, from
|
|
93
|
+
* the same `invoke` — an in-app agent and an external MCP client end at one execution path, so one
|
|
94
|
+
* policy decides both. It is not shared code and cannot be: `mcp` is this package's own tier, and
|
|
95
|
+
* the two projections narrow the schema differently on purpose (`toWireSchema` publishes only what
|
|
96
|
+
* that server's arg validator will hold a call to; this one publishes the tool schema the Messages
|
|
97
|
+
* API reads).
|
|
98
|
+
*
|
|
99
|
+
* `isAction` is structural against @ultimat3/action's PRIVATE declaration store, so a look-alike
|
|
100
|
+
* carrying `kind: 'action'` cannot take the first branch — it falls through as the already
|
|
101
|
+
* projectable object it claims to be.
|
|
102
|
+
*/
|
|
103
|
+
export function asProjectableAction(listed: AgentTool): ProjectableAction {
|
|
104
|
+
if (!isAction(listed)) return listed;
|
|
105
|
+
const mcp = listed.mcp;
|
|
106
|
+
return {
|
|
107
|
+
// Throws `X_ACTION_UNREGISTERED` on an unnamed action rather than offering a tool called `''`:
|
|
108
|
+
// a nameless tool is unaddressable by the model, by `runLlmToolCall` and by the author.
|
|
109
|
+
name: actionName(listed),
|
|
110
|
+
...(mcp === undefined ? {} : { mcp }),
|
|
111
|
+
...(mcp?.description === undefined ? {} : { description: mcp.description }),
|
|
112
|
+
inputJsonSchema: toMcpInputSchema(listed.input),
|
|
113
|
+
// The actor rides in on the options and `invoke` swaps it inside the one execution path —
|
|
114
|
+
// the action's own `policy` still decides, and its `input:` still parses what the model sent,
|
|
115
|
+
// which is what drops a `{ actor: 'admin' }` the model invented before any handler sees it.
|
|
116
|
+
run: ({ input, actor }) => invoke(listed, input, { surface: 'mcp', actor }),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The tool's name for an error message, before anything is registered. Never `actionName()`:
|
|
122
|
+
* `X_AGENT_TOOL_UNEXPOSED` is raised at declaration, where an action beside it in the same module
|
|
123
|
+
* has no name yet, and a naming failure there would hide the exposure failure being reported.
|
|
124
|
+
*/
|
|
125
|
+
export function toolLabel(listed: AgentTool): string {
|
|
126
|
+
return listed.name === '' ? '(an unregistered action)' : listed.name;
|
|
127
|
+
}
|
|
128
|
+
|
|
71
129
|
const EMPTY_SCHEMA: JsonSchema = { type: 'object', properties: {}, additionalProperties: false };
|
|
72
130
|
|
|
73
131
|
/** One action → one LLM tool definition. Opt-in via `mcp.expose`, same flag as MCP. */
|
|
@@ -108,18 +166,47 @@ export async function runLlmToolCall(
|
|
|
108
166
|
}
|
|
109
167
|
try {
|
|
110
168
|
const output = await action.run({ input: call.input, actor });
|
|
111
|
-
return
|
|
169
|
+
return resultOf(call.id, output);
|
|
112
170
|
} catch (error) {
|
|
113
171
|
// A policy denial is an outcome the model should read and react to, not a crash.
|
|
114
172
|
return { toolUseId: call.id, content: describeFailure(error), isError: true };
|
|
115
173
|
}
|
|
116
174
|
}
|
|
117
175
|
|
|
176
|
+
/**
|
|
177
|
+
* One tool's return value as the `tool_result` text. `content` is typed `string` and the agent
|
|
178
|
+
* loop TRUNCATES it, so anything else ends the run in a `TypeError` two frames away — and the
|
|
179
|
+
* value is an app's, so neither of `JSON.stringify`'s two other answers can be assumed away:
|
|
180
|
+
* `undefined` for a handler that returns nothing, and a throw on a bigint, a cycle or a `toJSON`
|
|
181
|
+
* of its own. A throw is reported as what it is — the call SUCCEEDED and its value cannot be
|
|
182
|
+
* read — never as a failure, because a model told the tool failed calls it again and buys its
|
|
183
|
+
* side effects twice.
|
|
184
|
+
*/
|
|
185
|
+
function resultOf(toolUseId: string, output: unknown): LlmToolResult {
|
|
186
|
+
let text: string | undefined;
|
|
187
|
+
try {
|
|
188
|
+
text = JSON.stringify(output);
|
|
189
|
+
} catch {
|
|
190
|
+
return {
|
|
191
|
+
toolUseId,
|
|
192
|
+
content:
|
|
193
|
+
'the tool ran, but its result is not JSON (a bigint, a cycle, or a toJSON that threw) — do not call it again',
|
|
194
|
+
isError: true,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return { toolUseId, content: text ?? 'null' };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The thrown value, read STRUCTURALLY and totally. `stringField` from `@ultimat3/core`, never
|
|
202
|
+
* `typeof e.code === 'string'`: the value is whatever an app's handler, its driver or its SDK
|
|
203
|
+
* threw, so each read is a getter call or a `Proxy` trap — and it runs inside the catch block
|
|
204
|
+
* that has nothing left to answer the model with if the probe itself raises.
|
|
205
|
+
*/
|
|
118
206
|
function describeFailure(error: unknown): string {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
return fix === '' ? `${e.code}: ${cause}` : `${e.code}: ${cause} (fix: ${fix})`;
|
|
207
|
+
const code = stringField(error, 'code');
|
|
208
|
+
if (code === undefined) return 'tool failed';
|
|
209
|
+
const cause = stringField(error, 'cause') ?? 'unknown';
|
|
210
|
+
const fix = stringField(error, 'fix') ?? '';
|
|
211
|
+
return fix === '' ? `${code}: ${cause}` : `${code}: ${cause} (fix: ${fix})`;
|
|
125
212
|
}
|
package/src/wire.ts
CHANGED
|
@@ -42,7 +42,11 @@ export function parsePartialUsage(raw: unknown): Partial<TokenUsage> {
|
|
|
42
42
|
const usage: Partial<Record<keyof TokenUsage, number>> = {};
|
|
43
43
|
for (const [field, wire] of Object.entries(USAGE_FIELDS) as [keyof TokenUsage, string][]) {
|
|
44
44
|
const value = record[wire];
|
|
45
|
-
|
|
45
|
+
// Floored at zero, and finite: usage is the provider's number, a negative one becomes a
|
|
46
|
+
// negative `cost`, and `MemoryBudgetStore.add` reads a negative debit as a CREDIT — releasing
|
|
47
|
+
// an unspent reservation is one — so an unclamped `-1` tops the ledger up instead of
|
|
48
|
+
// under-reporting it. `NaN` propagates the same way through every later sum.
|
|
49
|
+
if (typeof value === 'number' && Number.isFinite(value)) usage[field] = Math.max(0, value);
|
|
46
50
|
}
|
|
47
51
|
return usage;
|
|
48
52
|
}
|
|
@@ -81,17 +85,23 @@ export function parseStopDetails(raw: unknown): StopDetails | undefined {
|
|
|
81
85
|
* retry rule in the gateway: an overloaded provider is retryable whether it says so with a
|
|
82
86
|
* 529 on the handshake or with an `overloaded_error` frame ten tokens in.
|
|
83
87
|
*/
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
88
|
+
// A `Map`, not an object literal: `type` is the PROVIDER's string on the one read below, and
|
|
89
|
+
// `ERROR_STATUS['constructor']` on an object answers the `Object` FUNCTION where
|
|
90
|
+
// `AiTransportError.status` is declared `number | undefined`. Same fix, same reason, as
|
|
91
|
+
// `openai-wire.ts`'s twin and `core`'s `error-retry.ts`.
|
|
92
|
+
const ERROR_STATUS: ReadonlyMap<string, number> = new Map(
|
|
93
|
+
Object.entries({
|
|
94
|
+
invalid_request_error: 400,
|
|
95
|
+
authentication_error: 401,
|
|
96
|
+
permission_error: 403,
|
|
97
|
+
not_found_error: 404,
|
|
98
|
+
request_too_large: 413,
|
|
99
|
+
rate_limit_error: 429,
|
|
100
|
+
api_error: 500,
|
|
101
|
+
timeout_error: 504,
|
|
102
|
+
overloaded_error: 529,
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
95
105
|
|
|
96
106
|
/**
|
|
97
107
|
* A 200 whose body carries an `error` object instead of an answer, refused — how a gateway in
|
|
@@ -298,7 +308,7 @@ function inBandFailure(error: Record<string, unknown>): AiTransportError {
|
|
|
298
308
|
const message = typeof error['message'] === 'string' ? error['message'] : type;
|
|
299
309
|
return new AiTransportError({
|
|
300
310
|
provider: 'anthropic',
|
|
301
|
-
status: ERROR_STATUS
|
|
311
|
+
status: ERROR_STATUS.get(type),
|
|
302
312
|
detail: message,
|
|
303
313
|
});
|
|
304
314
|
}
|