@animalabs/agent-framework 0.1.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/LICENSE +21 -0
- package/README.md +233 -0
- package/dist/src/agent.d.ts +130 -0
- package/dist/src/agent.d.ts.map +1 -0
- package/dist/src/agent.js +406 -0
- package/dist/src/agent.js.map +1 -0
- package/dist/src/api/mcp-server.d.ts +37 -0
- package/dist/src/api/mcp-server.d.ts.map +1 -0
- package/dist/src/api/mcp-server.js +598 -0
- package/dist/src/api/mcp-server.js.map +1 -0
- package/dist/src/api/server.d.ts +83 -0
- package/dist/src/api/server.d.ts.map +1 -0
- package/dist/src/api/server.js +785 -0
- package/dist/src/api/server.js.map +1 -0
- package/dist/src/api/types.d.ts +395 -0
- package/dist/src/api/types.d.ts.map +1 -0
- package/dist/src/api/types.js +5 -0
- package/dist/src/api/types.js.map +1 -0
- package/dist/src/framework.d.ts +309 -0
- package/dist/src/framework.d.ts.map +1 -0
- package/dist/src/framework.js +2309 -0
- package/dist/src/framework.js.map +1 -0
- package/dist/src/gate/event-gate.d.ts +75 -0
- package/dist/src/gate/event-gate.d.ts.map +1 -0
- package/dist/src/gate/event-gate.js +503 -0
- package/dist/src/gate/event-gate.js.map +1 -0
- package/dist/src/gate/index.d.ts +3 -0
- package/dist/src/gate/index.d.ts.map +1 -0
- package/dist/src/gate/index.js +2 -0
- package/dist/src/gate/index.js.map +1 -0
- package/dist/src/gate/types.d.ts +82 -0
- package/dist/src/gate/types.d.ts.map +1 -0
- package/dist/src/gate/types.js +9 -0
- package/dist/src/gate/types.js.map +1 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +18 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/mcpl/channel-registry.d.ts +141 -0
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -0
- package/dist/src/mcpl/channel-registry.js +647 -0
- package/dist/src/mcpl/channel-registry.js.map +1 -0
- package/dist/src/mcpl/checkpoint-manager.d.ts +79 -0
- package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -0
- package/dist/src/mcpl/checkpoint-manager.js +430 -0
- package/dist/src/mcpl/checkpoint-manager.js.map +1 -0
- package/dist/src/mcpl/errors.d.ts +52 -0
- package/dist/src/mcpl/errors.d.ts.map +1 -0
- package/dist/src/mcpl/errors.js +93 -0
- package/dist/src/mcpl/errors.js.map +1 -0
- package/dist/src/mcpl/feature-set-manager.d.ts +91 -0
- package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -0
- package/dist/src/mcpl/feature-set-manager.js +250 -0
- package/dist/src/mcpl/feature-set-manager.js.map +1 -0
- package/dist/src/mcpl/hook-orchestrator.d.ts +51 -0
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -0
- package/dist/src/mcpl/hook-orchestrator.js +217 -0
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -0
- package/dist/src/mcpl/index.d.ts +14 -0
- package/dist/src/mcpl/index.d.ts.map +1 -0
- package/dist/src/mcpl/index.js +21 -0
- package/dist/src/mcpl/index.js.map +1 -0
- package/dist/src/mcpl/inference-router.d.ts +67 -0
- package/dist/src/mcpl/inference-router.d.ts.map +1 -0
- package/dist/src/mcpl/inference-router.js +249 -0
- package/dist/src/mcpl/inference-router.js.map +1 -0
- package/dist/src/mcpl/push-handler.d.ts +60 -0
- package/dist/src/mcpl/push-handler.d.ts.map +1 -0
- package/dist/src/mcpl/push-handler.js +162 -0
- package/dist/src/mcpl/push-handler.js.map +1 -0
- package/dist/src/mcpl/scope-manager.d.ts +76 -0
- package/dist/src/mcpl/scope-manager.d.ts.map +1 -0
- package/dist/src/mcpl/scope-manager.js +213 -0
- package/dist/src/mcpl/scope-manager.js.map +1 -0
- package/dist/src/mcpl/server-connection.d.ts +174 -0
- package/dist/src/mcpl/server-connection.d.ts.map +1 -0
- package/dist/src/mcpl/server-connection.js +559 -0
- package/dist/src/mcpl/server-connection.js.map +1 -0
- package/dist/src/mcpl/server-registry.d.ts +63 -0
- package/dist/src/mcpl/server-registry.d.ts.map +1 -0
- package/dist/src/mcpl/server-registry.js +119 -0
- package/dist/src/mcpl/server-registry.js.map +1 -0
- package/dist/src/mcpl/types.d.ts +755 -0
- package/dist/src/mcpl/types.d.ts.map +1 -0
- package/dist/src/mcpl/types.js +48 -0
- package/dist/src/mcpl/types.js.map +1 -0
- package/dist/src/module-registry.d.ts +93 -0
- package/dist/src/module-registry.d.ts.map +1 -0
- package/dist/src/module-registry.js +386 -0
- package/dist/src/module-registry.js.map +1 -0
- package/dist/src/modules/api/index.d.ts +22 -0
- package/dist/src/modules/api/index.d.ts.map +1 -0
- package/dist/src/modules/api/index.js +63 -0
- package/dist/src/modules/api/index.js.map +1 -0
- package/dist/src/modules/discord/discord-js-client.d.ts +58 -0
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -0
- package/dist/src/modules/discord/discord-js-client.js +273 -0
- package/dist/src/modules/discord/discord-js-client.js.map +1 -0
- package/dist/src/modules/discord/index.d.ts +82 -0
- package/dist/src/modules/discord/index.d.ts.map +1 -0
- package/dist/src/modules/discord/index.js +959 -0
- package/dist/src/modules/discord/index.js.map +1 -0
- package/dist/src/modules/discord/types.d.ts +327 -0
- package/dist/src/modules/discord/types.d.ts.map +1 -0
- package/dist/src/modules/discord/types.js +5 -0
- package/dist/src/modules/discord/types.js.map +1 -0
- package/dist/src/modules/index.d.ts +11 -0
- package/dist/src/modules/index.d.ts.map +1 -0
- package/dist/src/modules/index.js +7 -0
- package/dist/src/modules/index.js.map +1 -0
- package/dist/src/modules/workspace/index.d.ts +59 -0
- package/dist/src/modules/workspace/index.d.ts.map +1 -0
- package/dist/src/modules/workspace/index.js +831 -0
- package/dist/src/modules/workspace/index.js.map +1 -0
- package/dist/src/modules/workspace/sync.d.ts +48 -0
- package/dist/src/modules/workspace/sync.d.ts.map +1 -0
- package/dist/src/modules/workspace/sync.js +253 -0
- package/dist/src/modules/workspace/sync.js.map +1 -0
- package/dist/src/modules/workspace/types.d.ts +166 -0
- package/dist/src/modules/workspace/types.d.ts.map +1 -0
- package/dist/src/modules/workspace/types.js +5 -0
- package/dist/src/modules/workspace/types.js.map +1 -0
- package/dist/src/modules/workspace/watcher.d.ts +43 -0
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -0
- package/dist/src/modules/workspace/watcher.js +120 -0
- package/dist/src/modules/workspace/watcher.js.map +1 -0
- package/dist/src/queue.d.ts +50 -0
- package/dist/src/queue.d.ts.map +1 -0
- package/dist/src/queue.js +95 -0
- package/dist/src/queue.js.map +1 -0
- package/dist/src/safe-slice.d.ts +8 -0
- package/dist/src/safe-slice.d.ts.map +1 -0
- package/dist/src/safe-slice.js +18 -0
- package/dist/src/safe-slice.js.map +1 -0
- package/dist/src/types/agent.d.ts +118 -0
- package/dist/src/types/agent.d.ts.map +1 -0
- package/dist/src/types/agent.js +2 -0
- package/dist/src/types/agent.js.map +1 -0
- package/dist/src/types/events.d.ts +220 -0
- package/dist/src/types/events.d.ts.map +1 -0
- package/dist/src/types/events.js +2 -0
- package/dist/src/types/events.js.map +1 -0
- package/dist/src/types/framework.d.ts +184 -0
- package/dist/src/types/framework.d.ts.map +1 -0
- package/dist/src/types/framework.js +2 -0
- package/dist/src/types/framework.js.map +1 -0
- package/dist/src/types/index.d.ts +7 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +7 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/types/inference-log.d.ts +64 -0
- package/dist/src/types/inference-log.d.ts.map +1 -0
- package/dist/src/types/inference-log.js +5 -0
- package/dist/src/types/inference-log.js.map +1 -0
- package/dist/src/types/module.d.ts +302 -0
- package/dist/src/types/module.d.ts.map +1 -0
- package/dist/src/types/module.js +2 -0
- package/dist/src/types/module.js.map +1 -0
- package/dist/src/types/trace.d.ts +173 -0
- package/dist/src/types/trace.d.ts.map +1 -0
- package/dist/src/types/trace.js +2 -0
- package/dist/src/types/trace.js.map +1 -0
- package/dist/test/event-gate.test.d.ts +2 -0
- package/dist/test/event-gate.test.d.ts.map +1 -0
- package/dist/test/event-gate.test.js +535 -0
- package/dist/test/event-gate.test.js.map +1 -0
- package/dist/test/framework.test.d.ts +5 -0
- package/dist/test/framework.test.d.ts.map +1 -0
- package/dist/test/framework.test.js +738 -0
- package/dist/test/framework.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +58 -0
- package/src/agent.ts +512 -0
- package/src/api/mcp-server.ts +687 -0
- package/src/api/server.ts +1023 -0
- package/src/api/types.ts +542 -0
- package/src/framework.ts +2784 -0
- package/src/gate/event-gate.ts +623 -0
- package/src/gate/index.ts +12 -0
- package/src/gate/types.ts +112 -0
- package/src/index.ts +63 -0
- package/src/mcpl/channel-registry.ts +824 -0
- package/src/mcpl/checkpoint-manager.ts +545 -0
- package/src/mcpl/errors.ts +114 -0
- package/src/mcpl/feature-set-manager.ts +333 -0
- package/src/mcpl/hook-orchestrator.ts +281 -0
- package/src/mcpl/index.ts +134 -0
- package/src/mcpl/inference-router.ts +335 -0
- package/src/mcpl/push-handler.ts +232 -0
- package/src/mcpl/scope-manager.ts +262 -0
- package/src/mcpl/server-connection.ts +694 -0
- package/src/mcpl/server-registry.ts +144 -0
- package/src/mcpl/types.ts +991 -0
- package/src/module-registry.ts +547 -0
- package/src/modules/api/index.ts +94 -0
- package/src/modules/discord/discord-js-client.ts +353 -0
- package/src/modules/discord/index.ts +1146 -0
- package/src/modules/discord/types.ts +465 -0
- package/src/modules/index.ts +43 -0
- package/src/modules/workspace/index.ts +969 -0
- package/src/modules/workspace/sync.ts +314 -0
- package/src/modules/workspace/types.ts +202 -0
- package/src/modules/workspace/watcher.ts +142 -0
- package/src/queue.ts +106 -0
- package/src/safe-slice.ts +18 -0
- package/src/types/agent.ts +119 -0
- package/src/types/events.ts +252 -0
- package/src/types/framework.ts +211 -0
- package/src/types/index.ts +6 -0
- package/src/types/inference-log.ts +64 -0
- package/src/types/module.ts +367 -0
- package/src/types/trace.ts +196 -0
package/src/agent.ts
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import type { Membrane, NormalizedMessage, NormalizedRequest, ContentBlock, YieldingStream } from '@animalabs/membrane';
|
|
2
|
+
import { isAbortedResponse } from '@animalabs/membrane';
|
|
3
|
+
|
|
4
|
+
export interface StartStreamResult {
|
|
5
|
+
stream: YieldingStream;
|
|
6
|
+
request: NormalizedRequest;
|
|
7
|
+
}
|
|
8
|
+
import type { ContextManager, TokenBudget, ContextInjection, CompileResult } from '@animalabs/context-manager';
|
|
9
|
+
import type {
|
|
10
|
+
AgentConfig,
|
|
11
|
+
AgentState,
|
|
12
|
+
PendingToolCall,
|
|
13
|
+
CompletedToolCall,
|
|
14
|
+
ToolCallId,
|
|
15
|
+
ToolCall,
|
|
16
|
+
ToolResult,
|
|
17
|
+
ToolDefinition,
|
|
18
|
+
AgentInfo,
|
|
19
|
+
InferenceResult,
|
|
20
|
+
InferenceOptions,
|
|
21
|
+
} from './types/index.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* An agent wraps a context manager and manages inference state.
|
|
25
|
+
*/
|
|
26
|
+
export class Agent {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly model: string;
|
|
29
|
+
readonly systemPrompt: string;
|
|
30
|
+
readonly allowedTools: 'all' | string[];
|
|
31
|
+
readonly triggerSources: 'all' | string[];
|
|
32
|
+
readonly maxTokens: number;
|
|
33
|
+
readonly temperature: number;
|
|
34
|
+
|
|
35
|
+
private _state: AgentState = { status: 'idle' };
|
|
36
|
+
private _inferenceStartedAt = 0;
|
|
37
|
+
private _streamId = 0;
|
|
38
|
+
lastStreamInputTokens = 0;
|
|
39
|
+
maxStreamTokens: number;
|
|
40
|
+
private contextManager: ContextManager;
|
|
41
|
+
private membrane: Membrane;
|
|
42
|
+
|
|
43
|
+
constructor(
|
|
44
|
+
config: AgentConfig,
|
|
45
|
+
contextManager: ContextManager,
|
|
46
|
+
membrane: Membrane
|
|
47
|
+
) {
|
|
48
|
+
this.name = config.name;
|
|
49
|
+
this.model = config.model;
|
|
50
|
+
this.systemPrompt = config.systemPrompt;
|
|
51
|
+
this.allowedTools = config.allowedTools ?? 'all';
|
|
52
|
+
this.triggerSources = config.triggerSources ?? 'all';
|
|
53
|
+
this.maxTokens = config.maxTokens ?? 4096;
|
|
54
|
+
this.temperature = config.temperature ?? 1;
|
|
55
|
+
this.maxStreamTokens = config.maxStreamTokens ?? 150_000;
|
|
56
|
+
this.contextManager = contextManager;
|
|
57
|
+
this.membrane = membrane;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get current agent state.
|
|
62
|
+
*/
|
|
63
|
+
get state(): AgentState {
|
|
64
|
+
return this._state;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Monotonically increasing stream generation counter.
|
|
69
|
+
* Used to guard stale driveStream handlers after a budget restart.
|
|
70
|
+
*/
|
|
71
|
+
get streamId(): number {
|
|
72
|
+
return this._streamId;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get agent info.
|
|
77
|
+
*/
|
|
78
|
+
get info(): AgentInfo {
|
|
79
|
+
return {
|
|
80
|
+
name: this.name,
|
|
81
|
+
model: this.model,
|
|
82
|
+
status: this._state.status,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if agent can use a specific tool.
|
|
88
|
+
*/
|
|
89
|
+
canUseTool(toolName: string): boolean {
|
|
90
|
+
if (this.allowedTools === 'all') {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
return this.allowedTools.includes(toolName);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if a source can trigger inference for this agent.
|
|
98
|
+
*/
|
|
99
|
+
canBeTriggeredBy(source: string): boolean {
|
|
100
|
+
if (this.triggerSources === 'all') {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
return this.triggerSources.includes(source);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ==========================================================================
|
|
107
|
+
// Composable Steps
|
|
108
|
+
// ==========================================================================
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Compile the context without injections.
|
|
112
|
+
* Step 1 of the inference pipeline — callers can inspect/modify the result
|
|
113
|
+
* before building a request.
|
|
114
|
+
*/
|
|
115
|
+
async compileContext(budget?: TokenBudget): Promise<CompileResult> {
|
|
116
|
+
return this.contextManager.compile(budget);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Compile the context with injections.
|
|
121
|
+
* Same as compileContext but forwards injections (e.g. from MCPL servers)
|
|
122
|
+
* to the context manager so they are merged into the compiled messages.
|
|
123
|
+
*/
|
|
124
|
+
async compileWithInjections(
|
|
125
|
+
budget?: TokenBudget,
|
|
126
|
+
injections?: ContextInjection[]
|
|
127
|
+
): Promise<CompileResult> {
|
|
128
|
+
return this.contextManager.compile(budget, injections);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ==========================================================================
|
|
132
|
+
// Inference (backward-compatible)
|
|
133
|
+
// ==========================================================================
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Run inference and return result with tool calls and speech content.
|
|
137
|
+
* Updates agent state during execution.
|
|
138
|
+
*/
|
|
139
|
+
async runInference(
|
|
140
|
+
availableTools: ToolDefinition[],
|
|
141
|
+
budget?: TokenBudget,
|
|
142
|
+
options: InferenceOptions = {}
|
|
143
|
+
): Promise<InferenceResult> {
|
|
144
|
+
return this.runInferenceWithInjections(availableTools, undefined, budget, options);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Run inference with context injections.
|
|
149
|
+
* Same as runInference but passes injections through to compile.
|
|
150
|
+
*/
|
|
151
|
+
async runInferenceWithInjections(
|
|
152
|
+
availableTools: ToolDefinition[],
|
|
153
|
+
injections?: ContextInjection[],
|
|
154
|
+
budget?: TokenBudget,
|
|
155
|
+
options?: InferenceOptions
|
|
156
|
+
): Promise<InferenceResult> {
|
|
157
|
+
if (this._state.status === 'inferring') {
|
|
158
|
+
throw new Error(`Agent ${this.name} is already inferring`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (this._state.status === 'waiting_for_tools') {
|
|
162
|
+
throw new Error(`Agent ${this.name} is waiting for tool results`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Filter tools to only allowed ones
|
|
166
|
+
const tools = availableTools.filter((t) => this.canUseTool(t.name));
|
|
167
|
+
|
|
168
|
+
// Compile context (with optional injections)
|
|
169
|
+
const { messages, systemInjections } = await this.compileWithInjections(budget, injections);
|
|
170
|
+
|
|
171
|
+
// If we have pending tool results, add them
|
|
172
|
+
if (this._state.status === 'ready') {
|
|
173
|
+
const toolResultMessages = this.buildToolResultMessages(this._state.toolResults);
|
|
174
|
+
messages.push(...toolResultMessages);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const request: NormalizedRequest = {
|
|
178
|
+
messages,
|
|
179
|
+
system: this.buildSystemPrompt(systemInjections),
|
|
180
|
+
config: {
|
|
181
|
+
model: this.model,
|
|
182
|
+
maxTokens: this.maxTokens,
|
|
183
|
+
temperature: this.temperature,
|
|
184
|
+
},
|
|
185
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
186
|
+
assistantParticipant: this.name,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const abortController = new AbortController();
|
|
190
|
+
if (options?.signal) {
|
|
191
|
+
if (options.signal.aborted) {
|
|
192
|
+
abortController.abort();
|
|
193
|
+
} else {
|
|
194
|
+
options.signal.addEventListener('abort', () => abortController.abort(), { once: true });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Set state to inferring
|
|
199
|
+
this._inferenceStartedAt = Date.now();
|
|
200
|
+
const inferencePromise = this.doInference(request, abortController.signal);
|
|
201
|
+
this._state = { status: 'inferring', promise: inferencePromise, abortController };
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const result = await inferencePromise;
|
|
205
|
+
|
|
206
|
+
if (result.aborted) {
|
|
207
|
+
this._state = { status: 'idle' };
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (result.toolCalls.length > 0) {
|
|
212
|
+
// Waiting for tool results
|
|
213
|
+
const pending = new Map<ToolCallId, PendingToolCall>();
|
|
214
|
+
for (const call of result.toolCalls) {
|
|
215
|
+
pending.set(call.id, {
|
|
216
|
+
id: call.id,
|
|
217
|
+
name: call.name,
|
|
218
|
+
input: call.input,
|
|
219
|
+
startedAt: Date.now(),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
this._state = { status: 'waiting_for_tools', pending, completed: [] };
|
|
223
|
+
} else {
|
|
224
|
+
// Done, back to idle
|
|
225
|
+
this._state = { status: 'idle' };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return result;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
// On error, go back to idle
|
|
231
|
+
this._state = { status: 'idle' };
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Provide a tool result.
|
|
238
|
+
*/
|
|
239
|
+
provideToolResult(callId: ToolCallId, result: ToolResult): void {
|
|
240
|
+
if (this._state.status !== 'waiting_for_tools') {
|
|
241
|
+
throw new Error(`Agent ${this.name} is not waiting for tools`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const pending = this._state.pending.get(callId);
|
|
245
|
+
if (!pending) {
|
|
246
|
+
throw new Error(`Unknown tool call: ${callId}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Move from pending to completed
|
|
250
|
+
const completed: CompletedToolCall = {
|
|
251
|
+
id: pending.id,
|
|
252
|
+
name: pending.name,
|
|
253
|
+
input: pending.input,
|
|
254
|
+
result,
|
|
255
|
+
durationMs: Date.now() - pending.startedAt,
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
this._state.pending.delete(callId);
|
|
259
|
+
this._state.completed.push(completed);
|
|
260
|
+
|
|
261
|
+
// If all tools done, transition to ready
|
|
262
|
+
if (this._state.pending.size === 0) {
|
|
263
|
+
this._state = { status: 'ready', toolResults: this._state.completed, stream: this._state.stream };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Start a yielding stream for inference.
|
|
269
|
+
* Returns the stream — the caller (framework) iterates it.
|
|
270
|
+
*/
|
|
271
|
+
async startStream(
|
|
272
|
+
availableTools: ToolDefinition[],
|
|
273
|
+
budget?: TokenBudget
|
|
274
|
+
): Promise<StartStreamResult> {
|
|
275
|
+
return this.startStreamWithInjections(availableTools, undefined, budget);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Start a yielding stream with context injections.
|
|
280
|
+
* Same as startStream but passes injections through to compile.
|
|
281
|
+
*/
|
|
282
|
+
async startStreamWithInjections(
|
|
283
|
+
availableTools: ToolDefinition[],
|
|
284
|
+
injections?: ContextInjection[],
|
|
285
|
+
budget?: TokenBudget
|
|
286
|
+
): Promise<StartStreamResult> {
|
|
287
|
+
if (this._state.status !== 'idle') {
|
|
288
|
+
throw new Error(`Agent ${this.name} cannot start stream in state ${this._state.status}`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
this._streamId++;
|
|
292
|
+
this._inferenceStartedAt = Date.now();
|
|
293
|
+
this.lastStreamInputTokens = 0;
|
|
294
|
+
|
|
295
|
+
let { messages, systemInjections } = await this.compileWithInjections(budget, injections);
|
|
296
|
+
|
|
297
|
+
// Safety: ensure messages don't end with an assistant message.
|
|
298
|
+
// Some models reject trailing assistant messages ("prefill not supported"),
|
|
299
|
+
// and after context compression a stale assistant turn can end up last.
|
|
300
|
+
if (messages.length > 0 && messages[messages.length - 1]!.participant === this.name) {
|
|
301
|
+
messages = [...messages, {
|
|
302
|
+
participant: 'user',
|
|
303
|
+
content: [{ type: 'text', text: '[Continue]' }],
|
|
304
|
+
}];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const request: NormalizedRequest = {
|
|
308
|
+
messages,
|
|
309
|
+
system: this.buildSystemPrompt(systemInjections),
|
|
310
|
+
config: {
|
|
311
|
+
model: this.model,
|
|
312
|
+
maxTokens: this.maxTokens,
|
|
313
|
+
temperature: this.temperature,
|
|
314
|
+
},
|
|
315
|
+
tools: availableTools.length > 0 ? availableTools : undefined,
|
|
316
|
+
promptCaching: true,
|
|
317
|
+
assistantParticipant: this.name,
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const stream = this.membrane.streamYielding(request, {
|
|
321
|
+
emitTokens: true,
|
|
322
|
+
emitBlocks: false,
|
|
323
|
+
emitUsage: true,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
this._state = { status: 'streaming', stream };
|
|
327
|
+
return { stream, request };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Transition to waiting_for_tools when stream yields tool calls.
|
|
332
|
+
* Called by framework's driveStream.
|
|
333
|
+
*/
|
|
334
|
+
enterWaitingForTools(calls: ToolCall[], stream: YieldingStream): void {
|
|
335
|
+
const pending = new Map<ToolCallId, PendingToolCall>();
|
|
336
|
+
for (const call of calls) {
|
|
337
|
+
pending.set(call.id, {
|
|
338
|
+
id: call.id,
|
|
339
|
+
name: call.name,
|
|
340
|
+
input: call.input,
|
|
341
|
+
startedAt: Date.now(),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
this._state = { status: 'waiting_for_tools', pending, completed: [], stream };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Add an assistant response to context.
|
|
349
|
+
* Called by framework when stream completes.
|
|
350
|
+
*/
|
|
351
|
+
addAssistantResponse(content: ContentBlock[]): void {
|
|
352
|
+
this.contextManager.addMessage(this.name, content);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Transition back to streaming state after tool results are provided.
|
|
357
|
+
*/
|
|
358
|
+
setStreaming(stream: YieldingStream): void {
|
|
359
|
+
this._state = { status: 'streaming', stream };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Cancel any active stream and reset to idle.
|
|
364
|
+
*/
|
|
365
|
+
cancelStream(): void {
|
|
366
|
+
if (this._state.status === 'streaming') {
|
|
367
|
+
this._state.stream.cancel();
|
|
368
|
+
} else if (this._state.status === 'waiting_for_tools' && this._state.stream) {
|
|
369
|
+
this._state.stream.cancel();
|
|
370
|
+
}
|
|
371
|
+
this._state = { status: 'idle' };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Check if agent has pending tool calls.
|
|
376
|
+
*/
|
|
377
|
+
hasPendingTools(): boolean {
|
|
378
|
+
return this._state.status === 'waiting_for_tools' && this._state.pending.size > 0;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Get pending tool call IDs.
|
|
383
|
+
*/
|
|
384
|
+
getPendingToolIds(): ToolCallId[] {
|
|
385
|
+
if (this._state.status !== 'waiting_for_tools') {
|
|
386
|
+
return [];
|
|
387
|
+
}
|
|
388
|
+
return Array.from(this._state.pending.keys());
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Reset agent to idle state.
|
|
393
|
+
*/
|
|
394
|
+
reset(): void {
|
|
395
|
+
this._state = { status: 'idle' };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Get the context manager.
|
|
400
|
+
*/
|
|
401
|
+
getContextManager(): ContextManager {
|
|
402
|
+
return this.contextManager;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Build the effective system prompt, appending any system-position injections.
|
|
407
|
+
*/
|
|
408
|
+
private buildSystemPrompt(systemInjections: ContentBlock[]): string {
|
|
409
|
+
if (systemInjections.length === 0) {
|
|
410
|
+
return this.systemPrompt;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const injectedText = systemInjections
|
|
414
|
+
.filter((block): block is ContentBlock & { type: 'text' } => block.type === 'text')
|
|
415
|
+
.map((block) => block.text);
|
|
416
|
+
|
|
417
|
+
if (injectedText.length === 0) {
|
|
418
|
+
return this.systemPrompt;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return this.systemPrompt + '\n' + injectedText.join('\n');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
abortInference(reason?: string): { aborted: true; durationMs: number } | false {
|
|
425
|
+
if (this._state.status === 'inferring') {
|
|
426
|
+
const durationMs = Date.now() - this._inferenceStartedAt;
|
|
427
|
+
this._state.abortController.abort(reason);
|
|
428
|
+
return { aborted: true, durationMs };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (this._state.status === 'streaming' ||
|
|
432
|
+
(this._state.status === 'waiting_for_tools' && this._state.stream)) {
|
|
433
|
+
const durationMs = Date.now() - this._inferenceStartedAt;
|
|
434
|
+
this.cancelStream();
|
|
435
|
+
return { aborted: true, durationMs };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private async doInference(
|
|
442
|
+
request: NormalizedRequest,
|
|
443
|
+
signal?: AbortSignal
|
|
444
|
+
): Promise<InferenceResult> {
|
|
445
|
+
const response = await this.membrane.stream(request, { signal });
|
|
446
|
+
|
|
447
|
+
if (isAbortedResponse(response)) {
|
|
448
|
+
const partialContent = response.partialContent ?? [];
|
|
449
|
+
const { toolCalls, speechContent } = this.extractToolCallsAndSpeech(partialContent);
|
|
450
|
+
return {
|
|
451
|
+
toolCalls,
|
|
452
|
+
speechContent,
|
|
453
|
+
usage: response.partialUsage,
|
|
454
|
+
stopReason: 'abort',
|
|
455
|
+
aborted: true,
|
|
456
|
+
abortReason: response.reason,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const { toolCalls, speechContent } = this.extractToolCallsAndSpeech(response.content);
|
|
461
|
+
|
|
462
|
+
// Add assistant response to context
|
|
463
|
+
this.contextManager.addMessage(this.name, response.content);
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
toolCalls,
|
|
467
|
+
speechContent,
|
|
468
|
+
raw: response.raw,
|
|
469
|
+
usage: response.usage,
|
|
470
|
+
stopReason: response.stopReason,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
private extractToolCallsAndSpeech(content: ContentBlock[]): {
|
|
475
|
+
toolCalls: ToolCall[];
|
|
476
|
+
speechContent: ContentBlock[];
|
|
477
|
+
} {
|
|
478
|
+
const toolCalls: ToolCall[] = [];
|
|
479
|
+
const speechContent: ContentBlock[] = [];
|
|
480
|
+
|
|
481
|
+
for (const block of content) {
|
|
482
|
+
if (block.type === 'tool_use') {
|
|
483
|
+
toolCalls.push({
|
|
484
|
+
id: block.id,
|
|
485
|
+
name: block.name,
|
|
486
|
+
input: block.input,
|
|
487
|
+
});
|
|
488
|
+
} else if (block.type === 'text') {
|
|
489
|
+
speechContent.push(block);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return { toolCalls, speechContent };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private buildToolResultMessages(results: CompletedToolCall[]): NormalizedMessage[] {
|
|
497
|
+
// Tool results go as a user message with tool_result blocks
|
|
498
|
+
const content = results.map((r) => ({
|
|
499
|
+
type: 'tool_result' as const,
|
|
500
|
+
toolUseId: r.id,
|
|
501
|
+
content: r.result.isError
|
|
502
|
+
? r.result.error ?? 'Unknown error'
|
|
503
|
+
: JSON.stringify(r.result.data),
|
|
504
|
+
isError: r.result.isError,
|
|
505
|
+
}));
|
|
506
|
+
|
|
507
|
+
return [{
|
|
508
|
+
participant: 'user',
|
|
509
|
+
content,
|
|
510
|
+
}];
|
|
511
|
+
}
|
|
512
|
+
}
|