agentic-kit 1.0.3 → 1.2.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/README.md +75 -32
- package/esm/event-stream.js +77 -0
- package/esm/index.js +151 -41
- package/esm/messages.js +130 -0
- package/esm/model-registry.js +26 -0
- package/esm/package.json +1 -0
- package/esm/provider-registry.js +20 -0
- package/esm/transform-messages.js +139 -0
- package/esm/types.js +1 -0
- package/event-stream.d.ts +19 -0
- package/event-stream.js +83 -0
- package/index.d.ts +26 -30
- package/index.js +179 -42
- package/messages.d.ts +31 -0
- package/messages.js +144 -0
- package/model-registry.d.ts +7 -0
- package/model-registry.js +34 -0
- package/package.json +13 -8
- package/provider-registry.d.ts +6 -0
- package/provider-registry.js +27 -0
- package/transform-messages.d.ts +2 -0
- package/transform-messages.js +142 -0
- package/types.d.ts +231 -0
- package/types.js +2 -0
package/README.md
CHANGED
|
@@ -12,7 +12,14 @@
|
|
|
12
12
|
<a href="https://www.npmjs.com/package/agentic-kit"><img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/agentic-kit?filename=packages%2Fagentic-kit%2Fpackage.json"/></a>
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
|
-
A
|
|
15
|
+
A low-level provider portability layer for LLM applications. `agentic-kit`
|
|
16
|
+
provides:
|
|
17
|
+
|
|
18
|
+
- provider-independent `ModelDescriptor` and `Context` types
|
|
19
|
+
- structured streaming events for text, reasoning, and tool calls
|
|
20
|
+
- model and provider registries
|
|
21
|
+
- cross-provider message normalization for replay and handoff
|
|
22
|
+
- a one-release compatibility wrapper for the legacy `AgentKit.generate()` API
|
|
16
23
|
|
|
17
24
|
## Installation
|
|
18
25
|
|
|
@@ -23,58 +30,94 @@ npm install agentic-kit
|
|
|
23
30
|
|
|
24
31
|
## Quick Start
|
|
25
32
|
|
|
33
|
+
### Structured API
|
|
34
|
+
|
|
26
35
|
```typescript
|
|
27
|
-
import {
|
|
36
|
+
import { complete, getModel } from 'agentic-kit';
|
|
28
37
|
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
38
|
+
const model = getModel('openai', 'gpt-4o-mini');
|
|
39
|
+
const message = await complete(model!, {
|
|
40
|
+
messages: [{ role: 'user', content: 'Hello', timestamp: Date.now() }],
|
|
41
|
+
});
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
const multi = createMultiProviderKit();
|
|
35
|
-
multi.addProvider(new OllamaAdapter('http://localhost:11434'));
|
|
36
|
-
const reply = await multi.generate({ model: 'mistral', prompt: 'Hello' });
|
|
43
|
+
console.log(message.content);
|
|
37
44
|
```
|
|
38
45
|
|
|
39
|
-
|
|
46
|
+
### Streaming
|
|
40
47
|
|
|
41
48
|
```typescript
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
import { stream, getModel } from 'agentic-kit';
|
|
50
|
+
|
|
51
|
+
const model = getModel('openai', 'gpt-4o-mini');
|
|
52
|
+
const result = stream(model!, {
|
|
53
|
+
messages: [{ role: 'user', content: 'Explain tool calling briefly.', timestamp: Date.now() }],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
for await (const event of result) {
|
|
57
|
+
if (event.type === 'text_delta') {
|
|
58
|
+
process.stdout.write(event.delta);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
46
61
|
```
|
|
47
62
|
|
|
48
63
|
## API Reference
|
|
49
64
|
|
|
50
|
-
###
|
|
65
|
+
### Core API
|
|
66
|
+
|
|
67
|
+
- `stream(model: ModelDescriptor, context: Context, options?: StreamOptions)`
|
|
68
|
+
- `complete(model: ModelDescriptor, context: Context, options?: StreamOptions)`
|
|
69
|
+
- `completeText(model: ModelDescriptor, context: Context, options?: StreamOptions)`
|
|
70
|
+
|
|
71
|
+
### Registry
|
|
72
|
+
|
|
73
|
+
- `registerModel(model)`, `registerModels(models)`, `clearModels()`
|
|
74
|
+
- `registerProvider(provider, sourceId?)`, `unregisterProviders(sourceId)`, `clearProviders()`
|
|
75
|
+
- `getModel(provider, modelId)`, `getModels(provider?)`, `getModelProviders()`
|
|
76
|
+
- `getProvider(api)`, `getRegisteredProviders()`
|
|
77
|
+
|
|
78
|
+
The package pre-registers `OpenAIAdapter`, `AnthropicAdapter`, and
|
|
79
|
+
`OllamaAdapter` (with empty credentials) plus the built-in model catalogs
|
|
80
|
+
from each adapter package. Re-register an adapter with your own API key to
|
|
81
|
+
activate it.
|
|
51
82
|
|
|
52
|
-
|
|
53
|
-
- `.addProvider(provider: AgentProvider): void`
|
|
54
|
-
- `.setProvider(name: string): void`
|
|
55
|
-
- `.listProviders(): string[]`
|
|
56
|
-
- `.getCurrentProvider(): AgentProvider | undefined`
|
|
83
|
+
### Message helpers
|
|
57
84
|
|
|
58
|
-
|
|
85
|
+
- `createUserMessage(content)`, `createAssistantMessage(model)`,
|
|
86
|
+
`createToolResultMessage(toolCallId, toolName, content, isError?)`
|
|
87
|
+
- `createTextContent(text?)`, `createImageContent(data, mimeType)`,
|
|
88
|
+
`createToolCall(id, name)`
|
|
89
|
+
- `getMessageText(assistant)` — concatenate text blocks.
|
|
90
|
+
- `cloneMessage(message)` — deep clone.
|
|
91
|
+
- `normalizeContext(context)` — apply per-message normalization.
|
|
92
|
+
- `injectDeferralResults(messages, options?)` — synthesize stand-in
|
|
93
|
+
`toolResult` messages for every `toolCall` that lacks both a decision and
|
|
94
|
+
a paired result. Useful when the user types a new message instead of
|
|
95
|
+
responding to a paused tool — the next request needs a well-formed
|
|
96
|
+
transcript.
|
|
97
|
+
- `transformMessages(messages, model)` — cross-provider normalization for
|
|
98
|
+
replay across different providers.
|
|
99
|
+
|
|
100
|
+
### Adapter re-exports
|
|
101
|
+
|
|
102
|
+
- `OpenAIAdapter`, `AnthropicAdapter`, `OllamaAdapter`
|
|
103
|
+
- Types: `OpenAIOptions`, `AnthropicOptions`
|
|
104
|
+
- `OllamaClient` for direct Ollama HTTP access.
|
|
105
|
+
|
|
106
|
+
### Legacy Compatibility API
|
|
107
|
+
|
|
108
|
+
`AgentKit` is still available for one transition release:
|
|
59
109
|
|
|
60
110
|
```ts
|
|
61
111
|
interface GenerateInput {
|
|
62
112
|
model: string;
|
|
63
|
-
prompt
|
|
113
|
+
prompt?: string;
|
|
114
|
+
messages?: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>;
|
|
64
115
|
stream?: boolean;
|
|
65
116
|
}
|
|
66
117
|
```
|
|
67
118
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
```ts
|
|
71
|
-
interface StreamingOptions {
|
|
72
|
-
onChunk?: (chunk: string) => void;
|
|
73
|
-
onStateChange?: (state: string) => void;
|
|
74
|
-
onError?: (error: Error) => void;
|
|
75
|
-
onComplete?: () => void;
|
|
76
|
-
}
|
|
77
|
-
```
|
|
119
|
+
Use `createOpenAIKit`, `createAnthropicKit`, `createOllamaKit`, or
|
|
120
|
+
`createMultiProviderKit()` if you still need the old prompt-only entrypoint.
|
|
78
121
|
|
|
79
122
|
---
|
|
80
123
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export class EventStream {
|
|
2
|
+
isTerminal;
|
|
3
|
+
extractResult;
|
|
4
|
+
queue = [];
|
|
5
|
+
waiting = [];
|
|
6
|
+
done = false;
|
|
7
|
+
finalResultPromise;
|
|
8
|
+
resolveFinalResult;
|
|
9
|
+
constructor(isTerminal, extractResult) {
|
|
10
|
+
this.isTerminal = isTerminal;
|
|
11
|
+
this.extractResult = extractResult;
|
|
12
|
+
this.finalResultPromise = new Promise((resolve) => {
|
|
13
|
+
this.resolveFinalResult = resolve;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
push(event) {
|
|
17
|
+
if (this.done) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (this.isTerminal(event)) {
|
|
21
|
+
this.done = true;
|
|
22
|
+
this.resolveFinalResult(this.extractResult(event));
|
|
23
|
+
}
|
|
24
|
+
const waiter = this.waiting.shift();
|
|
25
|
+
if (waiter) {
|
|
26
|
+
waiter({ value: event, done: false });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
this.queue.push(event);
|
|
30
|
+
}
|
|
31
|
+
end(result) {
|
|
32
|
+
this.done = true;
|
|
33
|
+
if (result !== undefined) {
|
|
34
|
+
this.resolveFinalResult(result);
|
|
35
|
+
}
|
|
36
|
+
while (this.waiting.length > 0) {
|
|
37
|
+
this.waiting.shift()({ value: undefined, done: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async *[Symbol.asyncIterator]() {
|
|
41
|
+
while (true) {
|
|
42
|
+
if (this.queue.length > 0) {
|
|
43
|
+
yield this.queue.shift();
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (this.done) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const next = await new Promise((resolve) => {
|
|
50
|
+
this.waiting.push(resolve);
|
|
51
|
+
});
|
|
52
|
+
if (next.done) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
yield next.value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
result() {
|
|
59
|
+
return this.finalResultPromise;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export class DefaultAssistantMessageEventStream extends EventStream {
|
|
63
|
+
constructor() {
|
|
64
|
+
super((event) => event.type === 'done' || event.type === 'error', (event) => {
|
|
65
|
+
if (event.type === 'done') {
|
|
66
|
+
return event.message;
|
|
67
|
+
}
|
|
68
|
+
if (event.type === 'error') {
|
|
69
|
+
return event.error;
|
|
70
|
+
}
|
|
71
|
+
throw new Error('Unexpected terminal event');
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function createAssistantMessageEventStream() {
|
|
76
|
+
return new DefaultAssistantMessageEventStream();
|
|
77
|
+
}
|
package/esm/index.js
CHANGED
|
@@ -1,74 +1,109 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import { OpenAIAdapter } from '@agentic-kit/openai';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
import { ANTHROPIC_MODELS, AnthropicAdapter } from '@agentic-kit/anthropic';
|
|
2
|
+
import { OLLAMA_MODELS, OllamaAdapter, OllamaClient } from '@agentic-kit/ollama';
|
|
3
|
+
import { OPENAI_COMPATIBLE_MODELS, OpenAIAdapter, } from '@agentic-kit/openai';
|
|
4
|
+
import { createAssistantMessageEventStream, EventStream } from './event-stream.js';
|
|
5
|
+
import { getMessageText, normalizeContext } from './messages.js';
|
|
6
|
+
import { clearModels, getModel, getModels, getProviders as getModelProviders, registerModel, registerModels, } from './model-registry.js';
|
|
7
|
+
import { clearProviders, getProvider as getRegisteredProvider, getProviders as getRegisteredProviders, registerProvider, unregisterProviders, } from './provider-registry.js';
|
|
8
|
+
import { transformMessages } from './transform-messages.js';
|
|
9
|
+
export * from './event-stream.js';
|
|
10
|
+
export * from './messages.js';
|
|
11
|
+
export * from './transform-messages.js';
|
|
12
|
+
export * from './types.js';
|
|
13
|
+
export { createAssistantMessageEventStream, EventStream, OllamaClient };
|
|
14
|
+
export { AnthropicAdapter, OllamaAdapter, OpenAIAdapter };
|
|
15
|
+
registerModels([...OPENAI_COMPATIBLE_MODELS, ...ANTHROPIC_MODELS, ...OLLAMA_MODELS]);
|
|
16
|
+
registerProvider(new OpenAIAdapter());
|
|
17
|
+
registerProvider(new AnthropicAdapter({ apiKey: '' }));
|
|
18
|
+
registerProvider(new OllamaAdapter());
|
|
19
|
+
export function stream(model, context, options) {
|
|
20
|
+
const provider = getRegisteredProvider(model.api);
|
|
21
|
+
if (!provider) {
|
|
22
|
+
throw new Error(`No provider registered for api '${model.api}'`);
|
|
22
23
|
}
|
|
24
|
+
return streamWithProvider(provider, model, context, options);
|
|
25
|
+
}
|
|
26
|
+
export async function complete(model, context, options) {
|
|
27
|
+
const response = stream(model, context, options);
|
|
28
|
+
return response.result();
|
|
29
|
+
}
|
|
30
|
+
export async function completeText(model, context, options) {
|
|
31
|
+
const message = await complete(model, context, options);
|
|
32
|
+
return getMessageText(message);
|
|
23
33
|
}
|
|
24
|
-
// ─── AgentKit ─────────────────────────────────────────────────────────────────
|
|
25
34
|
export class AgentKit {
|
|
26
35
|
providers = new Map();
|
|
27
36
|
current;
|
|
28
37
|
addProvider(provider) {
|
|
29
|
-
this.providers.set(provider
|
|
30
|
-
if (!this.current)
|
|
38
|
+
this.providers.set(getProviderName(provider), provider);
|
|
39
|
+
if (!this.current) {
|
|
31
40
|
this.current = provider;
|
|
41
|
+
}
|
|
32
42
|
return this;
|
|
33
43
|
}
|
|
34
44
|
setProvider(name) {
|
|
35
45
|
const provider = this.providers.get(name);
|
|
36
|
-
if (!provider)
|
|
46
|
+
if (!provider) {
|
|
37
47
|
throw new Error(`Provider '${name}' not found`);
|
|
48
|
+
}
|
|
38
49
|
this.current = provider;
|
|
39
50
|
return this;
|
|
40
51
|
}
|
|
41
|
-
getCurrentProvider() {
|
|
42
|
-
|
|
52
|
+
getCurrentProvider() {
|
|
53
|
+
return this.current;
|
|
54
|
+
}
|
|
55
|
+
listProviders() {
|
|
56
|
+
return Array.from(this.providers.keys());
|
|
57
|
+
}
|
|
43
58
|
async generate(input, options) {
|
|
44
|
-
if (!this.current)
|
|
59
|
+
if (!this.current) {
|
|
45
60
|
throw new Error('No provider set. Call addProvider() first.');
|
|
46
|
-
|
|
61
|
+
}
|
|
62
|
+
const provider = this.current;
|
|
63
|
+
const model = createLegacyModel(provider, input.model, input.maxTokens);
|
|
64
|
+
const context = legacyInputToContext(input);
|
|
65
|
+
const streamOptions = {
|
|
66
|
+
maxTokens: input.maxTokens,
|
|
67
|
+
temperature: input.temperature,
|
|
68
|
+
};
|
|
69
|
+
if (options?.onChunk || input.stream) {
|
|
70
|
+
options?.onStateChange?.('streaming');
|
|
47
71
|
try {
|
|
48
|
-
|
|
49
|
-
|
|
72
|
+
const response = streamWithProvider(provider, model, context, streamOptions);
|
|
73
|
+
for await (const event of response) {
|
|
74
|
+
if (event.type === 'text_delta') {
|
|
75
|
+
options?.onChunk?.(event.delta);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
assertLegacyGenerateSucceeded(await response.result());
|
|
79
|
+
options?.onComplete?.();
|
|
50
80
|
}
|
|
51
|
-
catch (
|
|
52
|
-
options
|
|
53
|
-
throw
|
|
81
|
+
catch (error) {
|
|
82
|
+
options?.onError?.(error);
|
|
83
|
+
throw error;
|
|
54
84
|
}
|
|
55
85
|
return;
|
|
56
86
|
}
|
|
57
87
|
try {
|
|
58
|
-
const
|
|
88
|
+
const message = assertLegacyGenerateSucceeded(await completeWithProvider(provider, model, context, streamOptions));
|
|
89
|
+
options?.onStateChange?.('complete');
|
|
90
|
+
const text = getMessageText(message);
|
|
59
91
|
options?.onComplete?.();
|
|
60
|
-
return
|
|
92
|
+
return text;
|
|
61
93
|
}
|
|
62
|
-
catch (
|
|
63
|
-
options?.onError?.(
|
|
64
|
-
throw
|
|
94
|
+
catch (error) {
|
|
95
|
+
options?.onError?.(error);
|
|
96
|
+
throw error;
|
|
65
97
|
}
|
|
66
98
|
}
|
|
67
99
|
async listModels() {
|
|
68
|
-
|
|
100
|
+
if (!this.current?.listModels) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
const models = await this.current.listModels();
|
|
104
|
+
return models.map((model) => (typeof model === 'string' ? model : model.id));
|
|
69
105
|
}
|
|
70
106
|
}
|
|
71
|
-
// ─── Factory helpers ──────────────────────────────────────────────────────────
|
|
72
107
|
export function createOllamaKit(baseUrl) {
|
|
73
108
|
return new AgentKit().addProvider(new OllamaAdapter(baseUrl));
|
|
74
109
|
}
|
|
@@ -81,3 +116,78 @@ export function createOpenAIKit(options) {
|
|
|
81
116
|
export function createMultiProviderKit() {
|
|
82
117
|
return new AgentKit();
|
|
83
118
|
}
|
|
119
|
+
export { clearModels, clearProviders, getModel, getModelProviders, getModels, getRegisteredProvider as getProvider, getRegisteredProviders, registerModel, registerModels, registerProvider, unregisterProviders, };
|
|
120
|
+
function legacyInputToContext(input) {
|
|
121
|
+
const messages = input.messages
|
|
122
|
+
? input.messages
|
|
123
|
+
.filter((message) => message.role !== 'system')
|
|
124
|
+
.map((message) => message.role === 'assistant'
|
|
125
|
+
? {
|
|
126
|
+
role: 'assistant',
|
|
127
|
+
api: 'legacy',
|
|
128
|
+
provider: 'legacy',
|
|
129
|
+
model: input.model,
|
|
130
|
+
content: [{ type: 'text', text: message.content }],
|
|
131
|
+
usage: {
|
|
132
|
+
input: 0,
|
|
133
|
+
output: 0,
|
|
134
|
+
cacheRead: 0,
|
|
135
|
+
cacheWrite: 0,
|
|
136
|
+
totalTokens: 0,
|
|
137
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
138
|
+
},
|
|
139
|
+
stopReason: 'stop',
|
|
140
|
+
timestamp: Date.now(),
|
|
141
|
+
}
|
|
142
|
+
: {
|
|
143
|
+
role: 'user',
|
|
144
|
+
content: message.content,
|
|
145
|
+
timestamp: Date.now(),
|
|
146
|
+
})
|
|
147
|
+
: [{ role: 'user', content: input.prompt ?? '', timestamp: Date.now() }];
|
|
148
|
+
const systemPrompt = input.system ?? input.messages?.find((message) => message.role === 'system')?.content;
|
|
149
|
+
return {
|
|
150
|
+
systemPrompt,
|
|
151
|
+
messages,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function createLegacyModel(provider, modelId, maxTokens) {
|
|
155
|
+
if (provider.createModel) {
|
|
156
|
+
return provider.createModel(modelId, {
|
|
157
|
+
maxOutputTokens: maxTokens,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
id: modelId,
|
|
162
|
+
name: modelId,
|
|
163
|
+
api: provider.api,
|
|
164
|
+
provider: getProviderName(provider),
|
|
165
|
+
baseUrl: '',
|
|
166
|
+
input: ['text'],
|
|
167
|
+
reasoning: false,
|
|
168
|
+
tools: true,
|
|
169
|
+
maxOutputTokens: maxTokens,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function streamWithProvider(provider, model, context, options) {
|
|
173
|
+
const normalized = normalizeContext(context);
|
|
174
|
+
const transformedContext = {
|
|
175
|
+
...normalized,
|
|
176
|
+
messages: transformMessages(normalized.messages, model),
|
|
177
|
+
};
|
|
178
|
+
return provider.stream(model, transformedContext, options);
|
|
179
|
+
}
|
|
180
|
+
async function completeWithProvider(provider, model, context, options) {
|
|
181
|
+
const response = streamWithProvider(provider, model, context, options);
|
|
182
|
+
return response.result();
|
|
183
|
+
}
|
|
184
|
+
function getProviderName(provider) {
|
|
185
|
+
return provider.name ?? provider.provider;
|
|
186
|
+
}
|
|
187
|
+
function assertLegacyGenerateSucceeded(message) {
|
|
188
|
+
if (message.stopReason === 'error' || message.stopReason === 'aborted') {
|
|
189
|
+
throw new Error(message.errorMessage ??
|
|
190
|
+
(message.stopReason === 'aborted' ? 'Generation aborted.' : 'Generation failed.'), { cause: message });
|
|
191
|
+
}
|
|
192
|
+
return message;
|
|
193
|
+
}
|
package/esm/messages.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
export function createEmptyUsage() {
|
|
2
|
+
return {
|
|
3
|
+
input: 0,
|
|
4
|
+
output: 0,
|
|
5
|
+
cacheRead: 0,
|
|
6
|
+
cacheWrite: 0,
|
|
7
|
+
totalTokens: 0,
|
|
8
|
+
cost: {
|
|
9
|
+
input: 0,
|
|
10
|
+
output: 0,
|
|
11
|
+
cacheRead: 0,
|
|
12
|
+
cacheWrite: 0,
|
|
13
|
+
total: 0,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function calculateUsageCost(model, usage) {
|
|
18
|
+
const schedule = model.cost;
|
|
19
|
+
usage.cost.input = ((schedule?.input ?? 0) / 1_000_000) * usage.input;
|
|
20
|
+
usage.cost.output = ((schedule?.output ?? 0) / 1_000_000) * usage.output;
|
|
21
|
+
usage.cost.cacheRead = ((schedule?.cacheRead ?? 0) / 1_000_000) * usage.cacheRead;
|
|
22
|
+
usage.cost.cacheWrite = ((schedule?.cacheWrite ?? 0) / 1_000_000) * usage.cacheWrite;
|
|
23
|
+
usage.cost.total =
|
|
24
|
+
usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
|
|
25
|
+
return usage.cost;
|
|
26
|
+
}
|
|
27
|
+
export function createAssistantMessage(model) {
|
|
28
|
+
return {
|
|
29
|
+
role: 'assistant',
|
|
30
|
+
api: model.api,
|
|
31
|
+
provider: model.provider,
|
|
32
|
+
model: model.id,
|
|
33
|
+
content: [],
|
|
34
|
+
usage: createEmptyUsage(),
|
|
35
|
+
stopReason: 'stop',
|
|
36
|
+
timestamp: Date.now(),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function createTextContent(text = '') {
|
|
40
|
+
return { type: 'text', text };
|
|
41
|
+
}
|
|
42
|
+
export function createImageContent(data, mimeType) {
|
|
43
|
+
return { type: 'image', data, mimeType };
|
|
44
|
+
}
|
|
45
|
+
export function createToolCall(id, name) {
|
|
46
|
+
return { type: 'toolCall', id, name, arguments: {}, rawArguments: '' };
|
|
47
|
+
}
|
|
48
|
+
export function createUserMessage(content) {
|
|
49
|
+
return {
|
|
50
|
+
role: 'user',
|
|
51
|
+
content,
|
|
52
|
+
timestamp: Date.now(),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function createToolResultMessage(toolCallId, toolName, content, isError = false) {
|
|
56
|
+
return {
|
|
57
|
+
role: 'toolResult',
|
|
58
|
+
toolCallId,
|
|
59
|
+
toolName,
|
|
60
|
+
content,
|
|
61
|
+
isError,
|
|
62
|
+
timestamp: Date.now(),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function getMessageText(message) {
|
|
66
|
+
return message.content
|
|
67
|
+
.filter((block) => block.type === 'text')
|
|
68
|
+
.map((block) => block.text)
|
|
69
|
+
.join('');
|
|
70
|
+
}
|
|
71
|
+
const DEFAULT_DEFERRAL_TEXT = 'User did not respond. Continue.';
|
|
72
|
+
/**
|
|
73
|
+
* Synthesize toolResult messages for every toolCall that lacks both a decision
|
|
74
|
+
* and a paired toolResult. Returns input unchanged (referentially equal) when
|
|
75
|
+
* nothing needs synthesizing.
|
|
76
|
+
*
|
|
77
|
+
* Use case: user types a message instead of clicking approve/deny on a paused
|
|
78
|
+
* tool. The agent can't resume on text alone — every dangling toolCall needs
|
|
79
|
+
* a result before the next request. Compose with sendMessages to forward the
|
|
80
|
+
* conversation cleanly:
|
|
81
|
+
*
|
|
82
|
+
* await sendMessages([...injectDeferralResults(messages), createUserMessage(text)]);
|
|
83
|
+
*/
|
|
84
|
+
export function injectDeferralResults(messages, optionsOrText = {}) {
|
|
85
|
+
const opts = typeof optionsOrText === 'string' ? { deferralText: optionsOrText } : optionsOrText;
|
|
86
|
+
const deferralText = opts.deferralText ?? DEFAULT_DEFERRAL_TEXT;
|
|
87
|
+
const completed = new Set();
|
|
88
|
+
for (const m of messages) {
|
|
89
|
+
if (m.role === 'toolResult')
|
|
90
|
+
completed.add(m.toolCallId);
|
|
91
|
+
}
|
|
92
|
+
const synthetic = [];
|
|
93
|
+
for (const m of messages) {
|
|
94
|
+
if (m.role !== 'assistant')
|
|
95
|
+
continue;
|
|
96
|
+
for (const block of m.content) {
|
|
97
|
+
if (block.type !== 'toolCall')
|
|
98
|
+
continue;
|
|
99
|
+
if (completed.has(block.id))
|
|
100
|
+
continue;
|
|
101
|
+
if ('decision' in block && block.decision !== undefined)
|
|
102
|
+
continue;
|
|
103
|
+
const result = createToolResultMessage(block.id, block.name, [
|
|
104
|
+
{ type: 'text', text: deferralText },
|
|
105
|
+
]);
|
|
106
|
+
if (opts.details !== undefined)
|
|
107
|
+
result.details = opts.details;
|
|
108
|
+
synthetic.push(result);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (synthetic.length === 0)
|
|
112
|
+
return messages;
|
|
113
|
+
return [...messages, ...synthetic];
|
|
114
|
+
}
|
|
115
|
+
export function cloneMessage(message) {
|
|
116
|
+
return JSON.parse(JSON.stringify(message));
|
|
117
|
+
}
|
|
118
|
+
export function normalizeContext(context) {
|
|
119
|
+
return {
|
|
120
|
+
systemPrompt: context.systemPrompt,
|
|
121
|
+
tools: context.tools ?? [],
|
|
122
|
+
messages: context.messages.map((message) => ensureTimestamp(cloneMessage(message))),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function ensureTimestamp(message) {
|
|
126
|
+
if (typeof message.timestamp !== 'number') {
|
|
127
|
+
message.timestamp = Date.now();
|
|
128
|
+
}
|
|
129
|
+
return message;
|
|
130
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const modelsByProvider = new Map();
|
|
2
|
+
export function registerModel(model) {
|
|
3
|
+
const providerModels = modelsByProvider.get(model.provider) ?? new Map();
|
|
4
|
+
providerModels.set(model.id, model);
|
|
5
|
+
modelsByProvider.set(model.provider, providerModels);
|
|
6
|
+
}
|
|
7
|
+
export function registerModels(models) {
|
|
8
|
+
for (const model of models) {
|
|
9
|
+
registerModel(model);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function getModel(provider, modelId) {
|
|
13
|
+
return modelsByProvider.get(provider)?.get(modelId);
|
|
14
|
+
}
|
|
15
|
+
export function getModels(provider) {
|
|
16
|
+
if (provider) {
|
|
17
|
+
return Array.from(modelsByProvider.get(provider)?.values() ?? []);
|
|
18
|
+
}
|
|
19
|
+
return Array.from(modelsByProvider.values()).flatMap((entries) => Array.from(entries.values()));
|
|
20
|
+
}
|
|
21
|
+
export function getProviders() {
|
|
22
|
+
return Array.from(modelsByProvider.keys());
|
|
23
|
+
}
|
|
24
|
+
export function clearModels() {
|
|
25
|
+
modelsByProvider.clear();
|
|
26
|
+
}
|
package/esm/package.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const providersByApi = new Map();
|
|
2
|
+
export function registerProvider(adapter, sourceId) {
|
|
3
|
+
providersByApi.set(adapter.api, { adapter, sourceId });
|
|
4
|
+
}
|
|
5
|
+
export function getProvider(api) {
|
|
6
|
+
return providersByApi.get(api)?.adapter;
|
|
7
|
+
}
|
|
8
|
+
export function getProviders() {
|
|
9
|
+
return Array.from(providersByApi.values(), (entry) => entry.adapter);
|
|
10
|
+
}
|
|
11
|
+
export function unregisterProviders(sourceId) {
|
|
12
|
+
for (const [api, entry] of providersByApi.entries()) {
|
|
13
|
+
if (entry.sourceId === sourceId) {
|
|
14
|
+
providersByApi.delete(api);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function clearProviders() {
|
|
19
|
+
providersByApi.clear();
|
|
20
|
+
}
|