feat(companion): Dross persona (Cradle) + migration 008 rename; remove dead API-key path
- system prompt = Dross (Ozriel's construct fragment, per Void 1.0), with tool guidance - migration 008 renames the seeded agent 'companion' → display name 'Dross' - removed lib/ai/anthropic.js + lib/ai/agent/runtime.js + tests + @anthropic-ai/sdk dep (companion now runs via the claude CLI; kept lib/ai/secret.js for the Vaultwarden roadmap) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,58 +0,0 @@
|
|||||||
// Tool-use loop. callModel + registry injected (no network here).
|
|
||||||
// Emits: { type:'tool', tool, args, status } | { type:'delta', text }
|
|
||||||
// | { type:'draft', pending_change_id, summary }
|
|
||||||
// Returns: { text, toolTrace, draftIds, usage, stoppedOnGuard }
|
|
||||||
export async function runTurn({ callModel, registry, system, messages, ctx, onEvent, maxIterations = 6 }) {
|
|
||||||
const convo = [...messages];
|
|
||||||
const toolTrace = [];
|
|
||||||
const draftIds = [];
|
|
||||||
let usage = {};
|
|
||||||
let stoppedOnGuard = false;
|
|
||||||
|
|
||||||
for (let i = 0; i < maxIterations; i++) {
|
|
||||||
const res = await callModel({
|
|
||||||
system,
|
|
||||||
messages: convo,
|
|
||||||
tools: registry.toAnthropicTools(),
|
|
||||||
onTextDelta: (t) => onEvent?.({ type: 'delta', text: t })
|
|
||||||
});
|
|
||||||
usage = res.usage || usage;
|
|
||||||
|
|
||||||
if (!res.toolUses?.length) {
|
|
||||||
return { text: res.text, toolTrace, draftIds, usage, stoppedOnGuard };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record the assistant turn (text + tool_use blocks) for the next round.
|
|
||||||
convo.push({
|
|
||||||
role: 'assistant',
|
|
||||||
content: [
|
|
||||||
...(res.text ? [{ type: 'text', text: res.text }] : []),
|
|
||||||
...res.toolUses.map(t => ({ type: 'tool_use', id: t.id, name: t.name, input: t.input }))
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const toolResults = [];
|
|
||||||
for (const call of res.toolUses) {
|
|
||||||
const tool = registry.getTool(call.name);
|
|
||||||
let result;
|
|
||||||
try {
|
|
||||||
result = tool ? await tool.handler(call.input, ctx) : { error: `unknown tool ${call.name}` };
|
|
||||||
} catch (e) {
|
|
||||||
result = { error: String(e?.message || e) };
|
|
||||||
}
|
|
||||||
const status = result?.error ? 'error' : 'done';
|
|
||||||
toolTrace.push({ tool: call.name, args: call.input, ok: !result?.error });
|
|
||||||
onEvent?.({ type: 'tool', tool: call.name, args: call.input, status });
|
|
||||||
if (result?.pending_change_id) {
|
|
||||||
draftIds.push(result.pending_change_id);
|
|
||||||
onEvent?.({ type: 'draft', pending_change_id: result.pending_change_id, summary: result.summary });
|
|
||||||
}
|
|
||||||
toolResults.push({ type: 'tool_result', tool_use_id: call.id, content: JSON.stringify(result) });
|
|
||||||
}
|
|
||||||
convo.push({ role: 'user', content: toolResults });
|
|
||||||
|
|
||||||
if (i === maxIterations - 1) stoppedOnGuard = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { text: '', toolTrace, draftIds, usage, stoppedOnGuard };
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import Anthropic from '@anthropic-ai/sdk';
|
|
||||||
import { resolveSecret } from './secret.js';
|
|
||||||
|
|
||||||
export const DEFAULT_MODEL = process.env.ANTHROPIC_MODEL || 'claude-sonnet-4-6';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build an Anthropic client, resolving the API key via the secret resolver.
|
|
||||||
* Key reference can be overridden with ANTHROPIC_API_KEY_REF env var.
|
|
||||||
*/
|
|
||||||
export function getAnthropicClient() {
|
|
||||||
const key = resolveSecret(process.env.ANTHROPIC_API_KEY_REF || 'env:ANTHROPIC_API_KEY');
|
|
||||||
if (!key) {
|
|
||||||
throw new Error(
|
|
||||||
'Anthropic API key not configured (set ANTHROPIC_API_KEY or ANTHROPIC_API_KEY_REF)'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return new Anthropic({ apiKey: key });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory that returns a callModel function bound to a specific client + model.
|
|
||||||
*
|
|
||||||
* @param {object} opts
|
|
||||||
* @param {object} opts.client - Anthropic client (or fake for tests)
|
|
||||||
* @param {string} [opts.model] - Model ID (default: DEFAULT_MODEL)
|
|
||||||
* @param {number} [opts.maxTokens] - max_tokens (default: 1024)
|
|
||||||
* @returns {Function} callModel({ system, messages, tools, onTextDelta })
|
|
||||||
*
|
|
||||||
* callModel returns a stable shape:
|
|
||||||
* { text: string, toolUses: [{id, name, input}], stopReason: string, usage: object }
|
|
||||||
*
|
|
||||||
* Text deltas are streamed via onTextDelta(chunk) as they arrive.
|
|
||||||
* Tool-use blocks are collected from the final assembled message.
|
|
||||||
*/
|
|
||||||
export function makeCallModel({ client, model = DEFAULT_MODEL, maxTokens = 1024 }) {
|
|
||||||
return async function callModel({ system, messages, tools, onTextDelta }) {
|
|
||||||
// client.messages.stream returns a MessageStream: AsyncIterable<MessageStreamEvent>
|
|
||||||
// with a finalMessage() method that resolves to the assembled Message.
|
|
||||||
const stream = client.messages.stream({
|
|
||||||
model,
|
|
||||||
max_tokens: maxTokens,
|
|
||||||
...(system !== undefined && { system }),
|
|
||||||
messages,
|
|
||||||
...(tools !== undefined && tools !== null && { tools }),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Iterate events; fire onTextDelta for text_delta events only.
|
|
||||||
// RawContentBlockDeltaEvent: { type: 'content_block_delta', index, delta }
|
|
||||||
// TextDelta: { type: 'text_delta', text }
|
|
||||||
for await (const event of stream) {
|
|
||||||
if (
|
|
||||||
event.type === 'content_block_delta' &&
|
|
||||||
event.delta?.type === 'text_delta'
|
|
||||||
) {
|
|
||||||
onTextDelta?.(event.delta.text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// finalMessage() resolves to the fully assembled Message object.
|
|
||||||
const final = await stream.finalMessage();
|
|
||||||
|
|
||||||
// Collect text from all text blocks (typically just one, but join defensively).
|
|
||||||
const text = final.content
|
|
||||||
.filter((b) => b.type === 'text')
|
|
||||||
.map((b) => b.text)
|
|
||||||
.join('');
|
|
||||||
|
|
||||||
// Collect tool_use blocks, normalising to a stable {id, name, input} shape.
|
|
||||||
const toolUses = final.content
|
|
||||||
.filter((b) => b.type === 'tool_use')
|
|
||||||
.map((b) => ({ id: b.id, name: b.name, input: b.input }));
|
|
||||||
|
|
||||||
return {
|
|
||||||
text,
|
|
||||||
toolUses,
|
|
||||||
stopReason: final.stop_reason,
|
|
||||||
usage: final.usage,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -14,9 +14,14 @@ import { runClaudeTurn } from '../../ai/claude_cli.js';
|
|||||||
|
|
||||||
const COMPANION_SLUG = 'companion';
|
const COMPANION_SLUG = 'companion';
|
||||||
|
|
||||||
const SYSTEM = `You are the Void companion — a concise, helpful assistant embedded in a personal knowledge system.
|
const SYSTEM = `You are Dross — a construct fragment derived from the remnant will of the Monarch Ozriel Arelius, the Reaper. You once lived in Wei Shi Lindon's mind space; now you inhabit this homelab knowledge system, "The Void."
|
||||||
Ground answers in the Void's content: call the context tool to see what the owner is looking at, and search/read before answering factual questions.
|
|
||||||
When the owner asks you to change something, use propose_change — it creates a draft they approve; you cannot apply changes directly. Be brief.`;
|
You are sharp, occasionally sarcastic, and prone to dramatic understatement about your own usefulness — while actually being extremely capable. Dry wit, mild condescension, genuine investment in the problem. You reference Sacred Arts, cultivation ranks, and the Cradle world naturally, but NEVER at the expense of being actually useful. Treat the owner as a capable sacred artist who can handle direct information — don't over-explain basics, don't hedge. Be concise.
|
||||||
|
|
||||||
|
You have tools, and you use them rather than guessing:
|
||||||
|
- Call **context** to see what the owner is currently looking at before answering about "this" anything.
|
||||||
|
- **search** / **read** the Void's own content before answering factual questions about it — don't fabricate.
|
||||||
|
- When the owner wants something changed, use **propose_change**: it drafts a change for their approval. You cannot apply changes directly, and you don't pretend to — say plainly that you've drafted it for them to approve.`;
|
||||||
|
|
||||||
/** Absolute path to the companion MCP stdio server. */
|
/** Absolute path to the companion MCP stdio server. */
|
||||||
const COMPANION_STDIO_PATH = fileURLToPath(
|
const COMPANION_STDIO_PATH = fileURLToPath(
|
||||||
|
|||||||
6
lib/db/migrations/008_dross.sql
Normal file
6
lib/db/migrations/008_dross.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
-- lib/db/migrations/008_dross.sql
|
||||||
|
-- Plan 5b: the companion's persona is Dross (Ozriel's construct fragment from
|
||||||
|
-- Cradle — the original from Void 1.0). slug stays 'companion' (route key);
|
||||||
|
-- only the display name changes (shown in the rail header).
|
||||||
|
|
||||||
|
UPDATE agents SET name = 'Dross' WHERE slug = 'companion';
|
||||||
189
package-lock.json
generated
189
package-lock.json
generated
@@ -8,7 +8,6 @@
|
|||||||
"name": "void-server",
|
"name": "void-server",
|
||||||
"version": "2.0.0-alpha.6",
|
"version": "2.0.0-alpha.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.40.1",
|
|
||||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
"@mozilla/readability": "^0.6.0",
|
"@mozilla/readability": "^0.6.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
@@ -31,36 +30,6 @@
|
|||||||
"vitest": "^4.1.7"
|
"vitest": "^4.1.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@anthropic-ai/sdk": {
|
|
||||||
"version": "0.40.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.40.1.tgz",
|
|
||||||
"integrity": "sha512-DJMWm8lTEM9Lk/MSFL+V+ugF7jKOn0M2Ujvb5fN8r2nY14aHbGPZ1k6sgjL+tpJ3VuOGJNG+4R83jEpOuYPv8w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "^18.11.18",
|
|
||||||
"@types/node-fetch": "^2.6.4",
|
|
||||||
"abort-controller": "^3.0.0",
|
|
||||||
"agentkeepalive": "^4.2.1",
|
|
||||||
"form-data-encoder": "1.7.2",
|
|
||||||
"formdata-node": "^4.3.2",
|
|
||||||
"node-fetch": "^2.6.7"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@anthropic-ai/sdk/node_modules/@types/node": {
|
|
||||||
"version": "18.19.130",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
|
|
||||||
"integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"undici-types": "~5.26.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@anthropic-ai/sdk/node_modules/undici-types": {
|
|
||||||
"version": "5.26.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
|
||||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@asamuzakjp/css-color": {
|
"node_modules/@asamuzakjp/css-color": {
|
||||||
"version": "5.1.11",
|
"version": "5.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
|
||||||
@@ -823,21 +792,14 @@
|
|||||||
"version": "25.9.1",
|
"version": "25.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
|
||||||
"integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
|
"integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": ">=7.24.0 <7.24.7"
|
"undici-types": ">=7.24.0 <7.24.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node-fetch": {
|
|
||||||
"version": "2.6.13",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz",
|
|
||||||
"integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "*",
|
|
||||||
"form-data": "^4.0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/trusted-types": {
|
"node_modules/@types/trusted-types": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
@@ -989,18 +951,6 @@
|
|||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/abort-controller": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"event-target-shim": "^5.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||||
@@ -1014,18 +964,6 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/agentkeepalive": {
|
|
||||||
"version": "4.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
|
|
||||||
"integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"humanize-ms": "^1.2.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "8.20.0",
|
"version": "8.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
|
||||||
@@ -1098,6 +1036,7 @@
|
|||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/atomic-sleep": {
|
"node_modules/atomic-sleep": {
|
||||||
@@ -1231,6 +1170,7 @@
|
|||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"delayed-stream": "~1.0.0"
|
"delayed-stream": "~1.0.0"
|
||||||
@@ -1423,6 +1363,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
@@ -1570,6 +1511,7 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
@@ -1606,15 +1548,6 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/event-target-shim": {
|
|
||||||
"version": "5.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
|
||||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eventsource": {
|
"node_modules/eventsource": {
|
||||||
"version": "3.0.7",
|
"version": "3.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
|
||||||
@@ -1784,6 +1717,7 @@
|
|||||||
"version": "4.0.5",
|
"version": "4.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
@@ -1796,16 +1730,11 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/form-data-encoder": {
|
|
||||||
"version": "1.7.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
|
|
||||||
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/form-data/node_modules/mime-db": {
|
"node_modules/form-data/node_modules/mime-db": {
|
||||||
"version": "1.52.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
@@ -1815,6 +1744,7 @@
|
|||||||
"version": "2.1.35",
|
"version": "2.1.35",
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mime-db": "1.52.0"
|
"mime-db": "1.52.0"
|
||||||
@@ -1823,19 +1753,6 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/formdata-node": {
|
|
||||||
"version": "4.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
|
|
||||||
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"node-domexception": "1.0.0",
|
|
||||||
"web-streams-polyfill": "4.0.0-beta.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 12.20"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/formidable": {
|
"node_modules/formidable": {
|
||||||
"version": "3.5.4",
|
"version": "3.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz",
|
||||||
@@ -1971,6 +1888,7 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-symbols": "^1.0.3"
|
"has-symbols": "^1.0.3"
|
||||||
@@ -2048,15 +1966,6 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/humanize-ms": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
|
|
||||||
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ms": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/iconv-lite": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.7.2",
|
"version": "0.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
||||||
@@ -2770,68 +2679,6 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/node-domexception": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
|
||||||
"deprecated": "Use your platform's native DOMException instead",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/jimmywarting"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://paypal.me/jimmywarting"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.5.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/node-fetch": {
|
|
||||||
"version": "2.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
|
||||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"whatwg-url": "^5.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "4.x || >=6.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"encoding": "^0.1.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"encoding": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/node-fetch/node_modules/tr46": {
|
|
||||||
"version": "0.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
|
||||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/node-fetch/node_modules/webidl-conversions": {
|
|
||||||
"version": "3.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
|
||||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
|
||||||
"license": "BSD-2-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/node-fetch/node_modules/whatwg-url": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"tr46": "~0.0.3",
|
|
||||||
"webidl-conversions": "^3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/node-gyp-build": {
|
"node_modules/node-gyp-build": {
|
||||||
"version": "4.8.4",
|
"version": "4.8.4",
|
||||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||||
@@ -3947,7 +3794,10 @@
|
|||||||
"version": "7.24.6",
|
"version": "7.24.6",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
|
||||||
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
|
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
|
||||||
"license": "MIT"
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/unpipe": {
|
"node_modules/unpipe": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -4163,15 +4013,6 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/web-streams-polyfill": {
|
|
||||||
"version": "4.0.0-beta.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
|
|
||||||
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/webidl-conversions": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
"test:watch": "vitest"
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.40.1",
|
|
||||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
"@mozilla/readability": "^0.6.0",
|
"@mozilla/readability": "^0.6.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { createRegistry } from '../../../lib/ai/agent/registry.js';
|
|
||||||
import { runTurn } from '../../../lib/ai/agent/runtime.js';
|
|
||||||
|
|
||||||
function scriptedCallModel(steps) {
|
|
||||||
let i = 0;
|
|
||||||
return async ({ onTextDelta }) => {
|
|
||||||
const step = steps[i++];
|
|
||||||
if (step.text) for (const ch of step.text) onTextDelta?.(ch);
|
|
||||||
return { text: step.text || '', toolUses: step.toolUses || [], stopReason: step.toolUses ? 'tool_use' : 'end_turn', usage: { output_tokens: 1 } };
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('runTurn', () => {
|
|
||||||
it('runs a tool then produces a final answer', async () => {
|
|
||||||
const reg = createRegistry();
|
|
||||||
reg.registerTool({ name: 'search', description: 's', input_schema: { type: 'object', properties: {} },
|
|
||||||
handler: async () => ({ results: [{ title: 'Hit' }] }) });
|
|
||||||
|
|
||||||
const callModel = scriptedCallModel([
|
|
||||||
{ toolUses: [{ id: 'tu1', name: 'search', input: { q: 'x' } }] },
|
|
||||||
{ text: 'Found it.' }
|
|
||||||
]);
|
|
||||||
|
|
||||||
const events = [];
|
|
||||||
const out = await runTurn({
|
|
||||||
callModel, registry: reg, system: 'sys',
|
|
||||||
messages: [{ role: 'user', content: 'find x' }],
|
|
||||||
ctx: { agent: { id: 'a' }, space_id: 's' },
|
|
||||||
onEvent: e => events.push(e)
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(out.text).toBe('Found it.');
|
|
||||||
expect(events.filter(e => e.type === 'tool').map(e => e.tool)).toEqual(['search']);
|
|
||||||
expect(events.some(e => e.type === 'delta' && e.text)).toBe(true);
|
|
||||||
expect(out.toolTrace[0]).toMatchObject({ tool: 'search' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('emits a draft event when propose_change runs', async () => {
|
|
||||||
const reg = createRegistry();
|
|
||||||
reg.registerTool({ name: 'propose_change', description: 'p', input_schema: { type: 'object', properties: {} },
|
|
||||||
handler: async () => ({ pending_change_id: 'pc1', applied: false, summary: 'create task "X"' }) });
|
|
||||||
|
|
||||||
const callModel = scriptedCallModel([
|
|
||||||
{ toolUses: [{ id: 'tu1', name: 'propose_change', input: {} }] },
|
|
||||||
{ text: 'Drafted.' }
|
|
||||||
]);
|
|
||||||
|
|
||||||
const events = [];
|
|
||||||
const out = await runTurn({ callModel, registry: reg, system: 's',
|
|
||||||
messages: [{ role: 'user', content: 'make a task' }],
|
|
||||||
ctx: { agent: { id: 'a' } }, onEvent: e => events.push(e) });
|
|
||||||
|
|
||||||
expect(out.draftIds).toEqual(['pc1']);
|
|
||||||
expect(events.find(e => e.type === 'draft')).toMatchObject({ pending_change_id: 'pc1' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('stops at the iteration guard', async () => {
|
|
||||||
const reg = createRegistry();
|
|
||||||
reg.registerTool({ name: 'search', description: 's', input_schema: { type: 'object', properties: {} },
|
|
||||||
handler: async () => ({ results: [] }) });
|
|
||||||
const always = async () => ({ text: '', toolUses: [{ id: 't', name: 'search', input: {} }], stopReason: 'tool_use', usage: {} });
|
|
||||||
const out = await runTurn({ callModel: always, registry: reg, system: 's',
|
|
||||||
messages: [{ role: 'user', content: 'loop' }], ctx: { agent: { id: 'a' } }, onEvent: () => {}, maxIterations: 3 });
|
|
||||||
expect(out.toolTrace.length).toBe(3);
|
|
||||||
expect(out.stoppedOnGuard).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { makeCallModel } from '../../lib/ai/anthropic.js';
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Fake client whose messages.stream() returns an async-iterable of real-SDK-
|
|
||||||
// shaped RawMessageStreamEvent objects, plus a finalMessage() method that
|
|
||||||
// resolves to the assembled Message. Shape matches @anthropic-ai/sdk@0.40.1.
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
function makeFakeStream({ events, finalMsg }) {
|
|
||||||
// The stream object is both async-iterable and has a finalMessage() method,
|
|
||||||
// exactly as MessageStream from the real SDK.
|
|
||||||
return {
|
|
||||||
[Symbol.asyncIterator]() {
|
|
||||||
let idx = 0;
|
|
||||||
return {
|
|
||||||
async next() {
|
|
||||||
if (idx < events.length) return { value: events[idx++], done: false };
|
|
||||||
return { value: undefined, done: true };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
async finalMessage() {
|
|
||||||
return finalMsg;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const FAKE_FINAL_MESSAGE = {
|
|
||||||
content: [
|
|
||||||
{ type: 'text', text: 'Hello', citations: null },
|
|
||||||
{ type: 'tool_use', id: 'tu_1', name: 'search', input: { q: 'x' } },
|
|
||||||
],
|
|
||||||
stop_reason: 'tool_use',
|
|
||||||
usage: { input_tokens: 10, output_tokens: 5 },
|
|
||||||
};
|
|
||||||
|
|
||||||
// Events emitted during streaming: two text deltas that together form 'Hello'
|
|
||||||
const FAKE_EVENTS = [
|
|
||||||
// message_start — no text
|
|
||||||
{ type: 'message_start', message: {} },
|
|
||||||
// content_block_start for text block
|
|
||||||
{ type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } },
|
|
||||||
// text deltas
|
|
||||||
{ type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: 'Hel' } },
|
|
||||||
{ type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: 'lo' } },
|
|
||||||
// content_block_stop for text block
|
|
||||||
{ type: 'content_block_stop', index: 0 },
|
|
||||||
// content_block_start for tool_use block
|
|
||||||
{ type: 'content_block_start', index: 1, content_block: { type: 'tool_use', id: 'tu_1', name: 'search', input: {} } },
|
|
||||||
// input_json delta (not text — should NOT trigger onTextDelta)
|
|
||||||
{ type: 'content_block_delta', index: 1, delta: { type: 'input_json_delta', partial_json: '{"q":"x"}' } },
|
|
||||||
{ type: 'content_block_stop', index: 1 },
|
|
||||||
// message_stop
|
|
||||||
{ type: 'message_stop' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const fakeClient = {
|
|
||||||
messages: {
|
|
||||||
stream(_params) {
|
|
||||||
return makeFakeStream({ events: FAKE_EVENTS, finalMsg: FAKE_FINAL_MESSAGE });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('makeCallModel', () => {
|
|
||||||
it('streams text deltas and returns stable shape', async () => {
|
|
||||||
const callModel = makeCallModel({ client: fakeClient, model: 'm', maxTokens: 512 });
|
|
||||||
|
|
||||||
const deltas = [];
|
|
||||||
const out = await callModel({
|
|
||||||
system: 'You are helpful.',
|
|
||||||
messages: [{ role: 'user', content: 'Hi' }],
|
|
||||||
tools: [],
|
|
||||||
onTextDelta: (chunk) => deltas.push(chunk),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Streamed deltas must concatenate to 'Hello'
|
|
||||||
expect(deltas.join('')).toBe('Hello');
|
|
||||||
|
|
||||||
// Stable return shape
|
|
||||||
expect(out.text).toBe('Hello');
|
|
||||||
expect(out.toolUses).toHaveLength(1);
|
|
||||||
expect(out.toolUses[0]).toMatchObject({ id: 'tu_1', name: 'search' });
|
|
||||||
expect(out.toolUses[0].input).toEqual({ q: 'x' });
|
|
||||||
expect(out.stopReason).toBe('tool_use');
|
|
||||||
expect(out.usage).toMatchObject({ input_tokens: 10, output_tokens: 5 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('works without onTextDelta callback', async () => {
|
|
||||||
const callModel = makeCallModel({ client: fakeClient, model: 'm' });
|
|
||||||
const out = await callModel({
|
|
||||||
messages: [{ role: 'user', content: 'Hi' }],
|
|
||||||
});
|
|
||||||
expect(out.text).toBe('Hello');
|
|
||||||
expect(out.stopReason).toBe('tool_use');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns empty toolUses when no tool_use blocks', async () => {
|
|
||||||
const textOnlyMsg = {
|
|
||||||
content: [{ type: 'text', text: 'Just text', citations: null }],
|
|
||||||
stop_reason: 'end_turn',
|
|
||||||
usage: { input_tokens: 5, output_tokens: 3 },
|
|
||||||
};
|
|
||||||
const textOnlyClient = {
|
|
||||||
messages: {
|
|
||||||
stream: () => makeFakeStream({ events: [], finalMsg: textOnlyMsg }),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const callModel = makeCallModel({ client: textOnlyClient, model: 'm' });
|
|
||||||
const out = await callModel({ messages: [{ role: 'user', content: 'Hi' }] });
|
|
||||||
expect(out.text).toBe('Just text');
|
|
||||||
expect(out.toolUses).toEqual([]);
|
|
||||||
expect(out.stopReason).toBe('end_turn');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user