@ultimat3/ai 1.2.0 → 3.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 +521 -0
- package/README.md +367 -6
- 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 +396 -0
- package/src/budget.ts +98 -11
- package/src/error-body.ts +44 -0
- package/src/errors.ts +144 -96
- package/src/eval-baseline.ts +1 -1
- package/src/eval-errors.ts +98 -0
- package/src/evals.ts +1 -1
- package/src/fix-line.evals.ts +35 -0
- package/src/fix-line.ts +27 -0
- package/src/fix-line.v1.baseline.json +12 -0
- package/src/gateway.ts +57 -19
- 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 +55 -9
- package/src/llm-stream.ts +171 -0
- package/src/llm.ts +203 -26
- package/src/models.ts +186 -49
- package/src/openai-body.ts +96 -0
- package/src/openai-messages.ts +174 -0
- package/src/openai-models.ts +84 -0
- package/src/openai-provider.ts +274 -0
- package/src/openai-wire.ts +339 -0
- package/src/pg-vector-sql.ts +5 -1
- package/src/pg-vector.ts +2 -1
- package/src/prompt.ts +1 -1
- package/src/provider.ts +112 -38
- package/src/rag.ts +27 -3
- package/src/redaction.ts +22 -0
- package/src/remote-embedder.ts +53 -6
- package/src/runtime.ts +29 -0
- package/src/tools.ts +107 -11
- package/src/vector.ts +0 -0
- package/src/wire.ts +41 -11
package/src/tools.ts
CHANGED
|
@@ -2,14 +2,26 @@
|
|
|
2
2
|
//
|
|
3
3
|
// This is the SAME projection @ultimat3/mcp performs, in a different wire format: an
|
|
4
4
|
// in-app agent calling a tool through the gateway and an external agent calling it over
|
|
5
|
-
// MCP both end at `
|
|
5
|
+
// MCP both end at the same `invoke`, so they authorize identically. There is no "LLM
|
|
6
6
|
// permissions" concept in Ultimate, because there is no second authz system.
|
|
7
7
|
//
|
|
8
|
+
// `run` below is `ProjectableAction`'s — the projection SEAM, which is what carries `invoke`.
|
|
9
|
+
// It is not a member of the action facade: an `action()` is `as`/`tool`/`openapi`/`job`/
|
|
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).
|
|
15
|
+
//
|
|
8
16
|
// The JSON Schema type and the projectable-primitive shape are declared here rather than
|
|
9
17
|
// imported from @ultimat3/mcp: that package is the same tier, so importing it would be a
|
|
10
18
|
// boundary error. Both packages describe the same structural contract.
|
|
11
19
|
|
|
20
|
+
import type { AnyAction } from '@ultimat3/action';
|
|
21
|
+
import { actionName, invoke, isAction } from '@ultimat3/action';
|
|
12
22
|
import type { Actor } from '@ultimat3/core';
|
|
23
|
+
import { isMcpExposed, stringField } from '@ultimat3/core';
|
|
24
|
+
import { toMcpInputSchema } from '@ultimat3/schema';
|
|
13
25
|
|
|
14
26
|
/** The JSON Schema subset the framework emits for tool arguments. */
|
|
15
27
|
export interface JsonSchema {
|
|
@@ -63,6 +75,57 @@ export interface ProjectableAction {
|
|
|
63
75
|
run(args: { input: unknown; actor: Actor }): Promise<unknown>;
|
|
64
76
|
}
|
|
65
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
|
+
|
|
66
129
|
const EMPTY_SCHEMA: JsonSchema = { type: 'object', properties: {}, additionalProperties: false };
|
|
67
130
|
|
|
68
131
|
/** One action → one LLM tool definition. Opt-in via `mcp.expose`, same flag as MCP. */
|
|
@@ -76,10 +139,14 @@ export function toLlmTool(action: ProjectableAction): LlmTool {
|
|
|
76
139
|
};
|
|
77
140
|
}
|
|
78
141
|
|
|
79
|
-
/**
|
|
142
|
+
/**
|
|
143
|
+
* Every exposed action as a tool definition, in stable name order. The gateway and MCP ask
|
|
144
|
+
* `isMcpExposed` — @ultimat3/core's one predicate — so an in-app agent and an external one are
|
|
145
|
+
* offered exactly the same tools.
|
|
146
|
+
*/
|
|
80
147
|
export function toLlmTools(actions: readonly ProjectableAction[]): readonly LlmTool[] {
|
|
81
148
|
return actions
|
|
82
|
-
.filter((a) => a.mcp
|
|
149
|
+
.filter((a) => isMcpExposed(a.mcp))
|
|
83
150
|
.map(toLlmTool)
|
|
84
151
|
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
85
152
|
}
|
|
@@ -93,24 +160,53 @@ export async function runLlmToolCall(
|
|
|
93
160
|
call: LlmToolCall,
|
|
94
161
|
actor: Actor,
|
|
95
162
|
): Promise<LlmToolResult> {
|
|
96
|
-
const action = actions.find((a) => a.name === call.name && a.mcp
|
|
163
|
+
const action = actions.find((a) => a.name === call.name && isMcpExposed(a.mcp));
|
|
97
164
|
if (action === undefined) {
|
|
98
165
|
return { toolUseId: call.id, content: `unknown tool: ${call.name}`, isError: true };
|
|
99
166
|
}
|
|
100
167
|
try {
|
|
101
168
|
const output = await action.run({ input: call.input, actor });
|
|
102
|
-
return
|
|
169
|
+
return resultOf(call.id, output);
|
|
103
170
|
} catch (error) {
|
|
104
171
|
// A policy denial is an outcome the model should read and react to, not a crash.
|
|
105
172
|
return { toolUseId: call.id, content: describeFailure(error), isError: true };
|
|
106
173
|
}
|
|
107
174
|
}
|
|
108
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
|
+
*/
|
|
109
206
|
function describeFailure(error: unknown): string {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
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})`;
|
|
116
212
|
}
|
package/src/vector.ts
CHANGED
|
Binary file
|
package/src/wire.ts
CHANGED
|
@@ -93,6 +93,27 @@ const ERROR_STATUS: Readonly<Record<string, number>> = {
|
|
|
93
93
|
overloaded_error: 529,
|
|
94
94
|
};
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* A 200 whose body carries an `error` object instead of an answer, refused — how a gateway in
|
|
98
|
+
* front of a model reports a fault it noticed after the headers were sent. Exported because the
|
|
99
|
+
* non-streaming read needs the identical rule and the identical status table: the envelope arrives
|
|
100
|
+
* on either transport, and one copy of the mapping is what keeps the gateway's retry decision the
|
|
101
|
+
* same on both. The twin of `openai-wire.ts`'s.
|
|
102
|
+
*/
|
|
103
|
+
export function throwInBandError(payload: Record<string, unknown>): void {
|
|
104
|
+
const error = asRecord(payload['error']);
|
|
105
|
+
if (error === undefined) return;
|
|
106
|
+
throw inBandFailure(error);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A tool call's arguments, or `{}`. Never a cast: `input` is untrusted, so a string or an array
|
|
111
|
+
* arriving under `Record<string, unknown>` is a type lie every later reader indexes into.
|
|
112
|
+
*/
|
|
113
|
+
export function asToolInput(value: unknown): Record<string, unknown> {
|
|
114
|
+
return asRecord(value) ?? {};
|
|
115
|
+
}
|
|
116
|
+
|
|
96
117
|
interface PendingTool {
|
|
97
118
|
readonly id: string;
|
|
98
119
|
readonly name: string;
|
|
@@ -156,7 +177,10 @@ export class MessageStream {
|
|
|
156
177
|
return {
|
|
157
178
|
text: this.text,
|
|
158
179
|
toolCalls: [...this.toolCalls],
|
|
159
|
-
|
|
180
|
+
// A refusal detail is a refusal whatever the stop reason says. `parseStopReason` answers
|
|
181
|
+
// `end_turn` for a spelling this build has never seen, and every consumer branches on the
|
|
182
|
+
// REASON, so the pair would read as a complete answer that happens to be empty.
|
|
183
|
+
stopReason: this.stopDetails === undefined ? this.stopReason : 'refusal',
|
|
160
184
|
stopDetails: this.stopDetails,
|
|
161
185
|
usage: this.usage,
|
|
162
186
|
};
|
|
@@ -237,8 +261,7 @@ export class MessageStream {
|
|
|
237
261
|
private inputOf(tool: PendingTool): Record<string, unknown> {
|
|
238
262
|
if (tool.json === '') return {};
|
|
239
263
|
try {
|
|
240
|
-
|
|
241
|
-
return asRecord(parsed) ?? {};
|
|
264
|
+
return asToolInput(JSON.parse(tool.json));
|
|
242
265
|
} catch (error) {
|
|
243
266
|
throw new AiTransportError({
|
|
244
267
|
provider: 'anthropic',
|
|
@@ -261,18 +284,25 @@ export class MessageStream {
|
|
|
261
284
|
return [];
|
|
262
285
|
}
|
|
263
286
|
|
|
287
|
+
/**
|
|
288
|
+
* An `error` EVENT is a failure whether or not it carried a detail — the event type is itself
|
|
289
|
+
* the report. A body has no such signal, which is why `throwInBandError` reads the object first.
|
|
290
|
+
*/
|
|
264
291
|
private onError(payload: Record<string, unknown>): never {
|
|
265
|
-
|
|
266
|
-
const type = typeof error?.['type'] === 'string' ? error['type'] : 'api_error';
|
|
267
|
-
const message = typeof error?.['message'] === 'string' ? error['message'] : type;
|
|
268
|
-
throw new AiTransportError({
|
|
269
|
-
provider: 'anthropic',
|
|
270
|
-
status: ERROR_STATUS[type],
|
|
271
|
-
detail: message,
|
|
272
|
-
});
|
|
292
|
+
throw inBandFailure(asRecord(payload['error']) ?? {});
|
|
273
293
|
}
|
|
274
294
|
}
|
|
275
295
|
|
|
296
|
+
function inBandFailure(error: Record<string, unknown>): AiTransportError {
|
|
297
|
+
const type = typeof error['type'] === 'string' ? error['type'] : 'api_error';
|
|
298
|
+
const message = typeof error['message'] === 'string' ? error['message'] : type;
|
|
299
|
+
return new AiTransportError({
|
|
300
|
+
provider: 'anthropic',
|
|
301
|
+
status: ERROR_STATUS[type],
|
|
302
|
+
detail: message,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
276
306
|
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
277
307
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined;
|
|
278
308
|
return value as Record<string, unknown>;
|