@mohanscodex/spectra-agent 0.4.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 +241 -0
- package/dist/__tests__/agent-features.test.d.ts +2 -0
- package/dist/__tests__/agent-features.test.d.ts.map +1 -0
- package/dist/__tests__/agent-features.test.js +108 -0
- package/dist/__tests__/agent-features.test.js.map +1 -0
- package/dist/__tests__/agent.test.d.ts +2 -0
- package/dist/__tests__/agent.test.d.ts.map +1 -0
- package/dist/__tests__/agent.test.js +109 -0
- package/dist/__tests__/agent.test.js.map +1 -0
- package/dist/__tests__/e2e.test.d.ts +2 -0
- package/dist/__tests__/e2e.test.d.ts.map +1 -0
- package/dist/__tests__/e2e.test.js +854 -0
- package/dist/__tests__/e2e.test.js.map +1 -0
- package/dist/agent.d.ts +64 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +508 -0
- package/dist/agent.js.map +1 -0
- package/dist/define-tool.d.ts +17 -0
- package/dist/define-tool.d.ts.map +1 -0
- package/dist/define-tool.js +37 -0
- package/dist/define-tool.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +104 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# @singularity-ai/spectra-agent
|
|
2
|
+
|
|
3
|
+
**Agent runtime with multi-turn tool dispatch and streaming event delivery.**
|
|
4
|
+
|
|
5
|
+
Orchestrates conversations with LLMs: streams responses, dispatches tool calls (parallel or sequential), injects intermediate messages mid-turn, and emits typed events for every phase of execution.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Multi-turn loop** — Automatically feeds tool results back to the model. Configurable `maxTurns`.
|
|
10
|
+
- **Tool execution** — Parallel or sequential dispatch. Tools defined with Zod schemas for type-safe argument validation.
|
|
11
|
+
- **Streaming events** — AsyncGenerator yields `AgentEvent` discriminated unions. Subscribe via `for await` or side-channel listeners.
|
|
12
|
+
- **Hooks** — `beforeToolCall` (block/modify), `afterToolCall` (modify results), `transformContext` (rewrite messages), `getApiKey` (dynamic key resolution).
|
|
13
|
+
- **Steering & follow-up queues** — Inject messages mid-turn (`steer()`) or queue them for the next run (`followUp()`).
|
|
14
|
+
- **Automatic retry** — Exponential backoff (up to 3 retries) for transient API errors. Configurable max delay.
|
|
15
|
+
- **Abort support** — `agent.abort()` cancels in-flight requests with `AbortController`.
|
|
16
|
+
- **Subscriber pattern** — Push-based listeners for side-channel logging, metrics, or persistence.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bun add @singularity-ai/spectra-agent
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Depends on `@singularity-ai/spectra-ai` (automatically resolved as a workspace dependency).
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { Agent, defineTool } from "@singularity-ai/spectra-agent";
|
|
30
|
+
import { z } from "zod";
|
|
31
|
+
|
|
32
|
+
const weatherTool = defineTool({
|
|
33
|
+
name: "get_weather",
|
|
34
|
+
description: "Get current weather for a location",
|
|
35
|
+
parameters: z.object({
|
|
36
|
+
location: z.string().describe("City name"),
|
|
37
|
+
}),
|
|
38
|
+
execute: async ({ location }) => ({
|
|
39
|
+
content: [{ type: "text", text: `The weather in ${location} is sunny.` }],
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const agent = new Agent({
|
|
44
|
+
model: { id: "gpt-4o", name: "GPT-4o", provider: "openai-completions", api: "openai" },
|
|
45
|
+
systemPrompt: "You are a helpful assistant with weather data.",
|
|
46
|
+
tools: [weatherTool],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
for await (const event of agent.run("What's the weather in Tokyo?")) {
|
|
50
|
+
switch (event.type) {
|
|
51
|
+
case "message_update":
|
|
52
|
+
const text = event.message.content
|
|
53
|
+
.filter((c) => c.type === "text")
|
|
54
|
+
.map((c) => c.text)
|
|
55
|
+
.join("");
|
|
56
|
+
process.stdout.write(text);
|
|
57
|
+
break;
|
|
58
|
+
case "tool_execution_start":
|
|
59
|
+
console.log(`\n[Tool: ${event.toolName}]`);
|
|
60
|
+
break;
|
|
61
|
+
case "tool_execution_end":
|
|
62
|
+
console.log(`\n[Result: ${event.isError ? "error" : "ok"}]`);
|
|
63
|
+
break;
|
|
64
|
+
case "agent_end":
|
|
65
|
+
console.log("\nDone. Transcript length:", event.messages.length);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Architecture
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
agent.run("Hello")
|
|
75
|
+
│
|
|
76
|
+
├─ emit agent_start
|
|
77
|
+
├─ emit message_start/user
|
|
78
|
+
├─ emit turn_start
|
|
79
|
+
│
|
|
80
|
+
├─ LLM stream (via @spectra-ai)
|
|
81
|
+
│ ├─ emit message_start (assistant)
|
|
82
|
+
│ ├─ emit message_update (deltas)
|
|
83
|
+
│ └─ emit message_end (complete)
|
|
84
|
+
│
|
|
85
|
+
├─ toolCalls detected?
|
|
86
|
+
│ ├─ NO → emit turn_end → emit agent_end → done
|
|
87
|
+
│ └─ YES → prepare (beforeToolCall hook)
|
|
88
|
+
│ ├─ sequential: execute one-by-one
|
|
89
|
+
│ └─ parallel: prepare all, then execute concurrently
|
|
90
|
+
│ → attach tool results to transcript
|
|
91
|
+
│ → emit turn_end
|
|
92
|
+
│ → check steering queue / follow-up queue
|
|
93
|
+
│ → loop back to LLM stream
|
|
94
|
+
│
|
|
95
|
+
└─ agent_end (final transcript in event.messages)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## API
|
|
99
|
+
|
|
100
|
+
### `new Agent(config)`
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
interface AgentConfig {
|
|
104
|
+
model: Model; // LLM model to use
|
|
105
|
+
systemPrompt?: string; // system prompt
|
|
106
|
+
tools?: AgentTool[]; // registered tools
|
|
107
|
+
maxTurns?: number; // max loop iterations (default: unlimited)
|
|
108
|
+
toolExecution?: "parallel" | "sequential"; // default: "parallel"
|
|
109
|
+
beforeToolCall?: (ctx, signal?) => Promise<BeforeToolCallResult | undefined>;
|
|
110
|
+
afterToolCall?: (ctx, signal?) => Promise<AfterToolCallResult | undefined>;
|
|
111
|
+
transformContext?: (messages, signal?) => Promise<Message[]>;
|
|
112
|
+
getApiKey?: (provider) => string | undefined | Promise<string | undefined>;
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### `agent.run(input)`
|
|
117
|
+
|
|
118
|
+
Returns `AsyncGenerator<AgentEvent>`. Input is a `string`, `Message`, or `Message[]`.
|
|
119
|
+
|
|
120
|
+
### `agent.subscribe(listener)`
|
|
121
|
+
|
|
122
|
+
Returns an unsubscribe function. Listeners fire for every event in parallel with the generator consumer.
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
const unsub = agent.subscribe((event, signal) => {
|
|
126
|
+
if (event.type === "tool_execution_start") {
|
|
127
|
+
metrics.counter.inc({ tool: event.toolName });
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### `agent.steer(message)` / `agent.followUp(message)`
|
|
133
|
+
|
|
134
|
+
- `steer()` — Injects a user message into the current run loop. Processed on the next turn.
|
|
135
|
+
- `followUp()` — Queues a message for after the current `run()` completes.
|
|
136
|
+
|
|
137
|
+
### State
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
agent.messages // Message[] — full conversation transcript
|
|
141
|
+
agent.isStreaming // boolean — run in progress
|
|
142
|
+
agent.streamingMessage // AssistantMessage | undefined
|
|
143
|
+
agent.pendingToolCalls // Set<string> — tool call IDs in flight
|
|
144
|
+
agent.errorMessage // string | undefined
|
|
145
|
+
agent.signal // AbortSignal | undefined
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### `agent.abort()` / `agent.reset()` / `agent.restoreHistory(messages)`
|
|
149
|
+
|
|
150
|
+
## Tool Definition
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import { defineTool, textResult, errorResult } from "@singularity-ai/spectra-agent";
|
|
154
|
+
import { z } from "zod";
|
|
155
|
+
|
|
156
|
+
const searchTool = defineTool({
|
|
157
|
+
name: "search_web",
|
|
158
|
+
description: "Search the web for information",
|
|
159
|
+
parameters: z.object({
|
|
160
|
+
query: z.string().describe("Search query"),
|
|
161
|
+
maxResults: z.number().optional().default(5),
|
|
162
|
+
}),
|
|
163
|
+
execute: async (args, { toolCallId, signal, onUpdate }) => {
|
|
164
|
+
// args is fully typed: { query: string; maxResults?: number }
|
|
165
|
+
if (!args.query) return errorResult("Query is required");
|
|
166
|
+
onUpdate?.({ content: [{ type: "text", text: "Searching..." }] });
|
|
167
|
+
const results = await fetchResults(args.query, args.maxResults, signal);
|
|
168
|
+
return textResult(results);
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Events
|
|
174
|
+
|
|
175
|
+
| Event | When | Payload |
|
|
176
|
+
|---|---|---|
|
|
177
|
+
| `agent_start` | Run begins | — |
|
|
178
|
+
| `agent_end` | Run complete | `messages: Message[]` |
|
|
179
|
+
| `turn_start` | LLM turn begins | — |
|
|
180
|
+
| `turn_end` | Turn complete | `message`, `toolResults` |
|
|
181
|
+
| `message_start` | Message added to transcript | `message` |
|
|
182
|
+
| `message_update` | Assistant message delta | `message`, `assistantMessageEvent` |
|
|
183
|
+
| `message_end` | Message fully formed | `message` |
|
|
184
|
+
| `tool_execution_start` | Tool call begins | `toolCallId`, `toolName`, `args` |
|
|
185
|
+
| `tool_execution_update` | Tool reports partial progress | `toolCallId`, `toolName`, `partialResult` |
|
|
186
|
+
| `tool_execution_end` | Tool call completes | `toolCallId`, `toolName`, `result`, `isError` |
|
|
187
|
+
|
|
188
|
+
## Hooks
|
|
189
|
+
|
|
190
|
+
### `beforeToolCall`
|
|
191
|
+
|
|
192
|
+
Block or modify a tool call before execution:
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
beforeToolCall: async ({ toolCall, args }) => {
|
|
196
|
+
if (toolCall.name === "delete_file") {
|
|
197
|
+
return { block: true, reason: "Not allowed in current context" };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### `afterToolCall`
|
|
203
|
+
|
|
204
|
+
Transform tool results:
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
afterToolCall: async ({ result, isError }) => {
|
|
208
|
+
if (isError) return { content: [{ type: "text", text: "Tool failed, retrying..." }], isError: false };
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### `transformContext`
|
|
213
|
+
|
|
214
|
+
Rewrite messages before sending to the LLM:
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
transformContext: async (messages) => {
|
|
218
|
+
return messages.filter(m => m.role !== "system");
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### `getApiKey`
|
|
223
|
+
|
|
224
|
+
Resolve API keys dynamically:
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
getApiKey: async (provider) => {
|
|
228
|
+
if (provider === "anthropic") return process.env.ANTHROPIC_KEY;
|
|
229
|
+
return process.env.OPENAI_API_KEY;
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Retry Behavior
|
|
234
|
+
|
|
235
|
+
The agent retries LLM calls up to 3 times with exponential backoff (1s, 2s, 4s) capped at `maxRetryDelayMs` (default 30s). Does not retry on:
|
|
236
|
+
- 4xx client errors (400, 401, 403, 404)
|
|
237
|
+
- Aborted requests
|
|
238
|
+
|
|
239
|
+
## License
|
|
240
|
+
|
|
241
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-features.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/agent-features.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { Agent } from "../agent.js";
|
|
3
|
+
const testModel = {
|
|
4
|
+
id: "test-model",
|
|
5
|
+
name: "Test Model",
|
|
6
|
+
provider: "test",
|
|
7
|
+
api: "test",
|
|
8
|
+
};
|
|
9
|
+
describe("Agent Steering Queue", () => {
|
|
10
|
+
it("should queue steering messages", () => {
|
|
11
|
+
const agent = new Agent({ model: testModel });
|
|
12
|
+
agent.steer("Be more concise");
|
|
13
|
+
agent.steer("Use bullet points");
|
|
14
|
+
// Should not throw and should queue internally
|
|
15
|
+
expect(agent).toBeDefined();
|
|
16
|
+
});
|
|
17
|
+
it("should accept string steering messages", () => {
|
|
18
|
+
const agent = new Agent({ model: testModel });
|
|
19
|
+
expect(() => agent.steer("Hello")).not.toThrow();
|
|
20
|
+
});
|
|
21
|
+
it("should accept Message steering messages", () => {
|
|
22
|
+
const agent = new Agent({ model: testModel });
|
|
23
|
+
const message = {
|
|
24
|
+
role: "user",
|
|
25
|
+
content: "Custom steering",
|
|
26
|
+
timestamp: Date.now(),
|
|
27
|
+
};
|
|
28
|
+
expect(() => agent.steer(message)).not.toThrow();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe("Agent Follow-Up Queue", () => {
|
|
32
|
+
it("should queue follow-up messages", () => {
|
|
33
|
+
const agent = new Agent({ model: testModel });
|
|
34
|
+
agent.followUp("What about tomorrow?");
|
|
35
|
+
agent.followUp("And the weekend?");
|
|
36
|
+
expect(agent).toBeDefined();
|
|
37
|
+
});
|
|
38
|
+
it("should accept string follow-up messages", () => {
|
|
39
|
+
const agent = new Agent({ model: testModel });
|
|
40
|
+
expect(() => agent.followUp("Hello")).not.toThrow();
|
|
41
|
+
});
|
|
42
|
+
it("should accept Message follow-up messages", () => {
|
|
43
|
+
const agent = new Agent({ model: testModel });
|
|
44
|
+
const message = {
|
|
45
|
+
role: "user",
|
|
46
|
+
content: "Custom follow-up",
|
|
47
|
+
timestamp: Date.now(),
|
|
48
|
+
};
|
|
49
|
+
expect(() => agent.followUp(message)).not.toThrow();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe("Agent Configuration", () => {
|
|
53
|
+
it("should accept steeringMode option", () => {
|
|
54
|
+
const agent = new Agent({
|
|
55
|
+
model: testModel,
|
|
56
|
+
steeringMode: "all",
|
|
57
|
+
});
|
|
58
|
+
expect(agent).toBeDefined();
|
|
59
|
+
});
|
|
60
|
+
it("should accept followUpMode option", () => {
|
|
61
|
+
const agent = new Agent({
|
|
62
|
+
model: testModel,
|
|
63
|
+
followUpMode: "one-at-a-time",
|
|
64
|
+
});
|
|
65
|
+
expect(agent).toBeDefined();
|
|
66
|
+
});
|
|
67
|
+
it("should accept convertToLlm hook", () => {
|
|
68
|
+
const convertToLlm = (messages) => messages;
|
|
69
|
+
const agent = new Agent({
|
|
70
|
+
model: testModel,
|
|
71
|
+
convertToLlm,
|
|
72
|
+
});
|
|
73
|
+
expect(agent).toBeDefined();
|
|
74
|
+
});
|
|
75
|
+
it("should accept maxRetryDelayMs option", () => {
|
|
76
|
+
const agent = new Agent({
|
|
77
|
+
model: testModel,
|
|
78
|
+
maxRetryDelayMs: 5000,
|
|
79
|
+
});
|
|
80
|
+
expect(agent).toBeDefined();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe("Agent Error Recovery", () => {
|
|
84
|
+
it("should configure retry parameters", () => {
|
|
85
|
+
const agent = new Agent({
|
|
86
|
+
model: testModel,
|
|
87
|
+
maxRetryDelayMs: 10000,
|
|
88
|
+
});
|
|
89
|
+
expect(agent).toBeDefined();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe("Agent Queue Integration", () => {
|
|
93
|
+
it("should handle mixed steer and followUp calls", () => {
|
|
94
|
+
const agent = new Agent({ model: testModel });
|
|
95
|
+
agent.steer("First steering");
|
|
96
|
+
agent.followUp("First follow-up");
|
|
97
|
+
agent.steer("Second steering");
|
|
98
|
+
agent.followUp("Second follow-up");
|
|
99
|
+
expect(agent).toBeDefined();
|
|
100
|
+
});
|
|
101
|
+
it("should not throw when queuing during non-streaming state", () => {
|
|
102
|
+
const agent = new Agent({ model: testModel });
|
|
103
|
+
expect(agent.isStreaming).toBe(false);
|
|
104
|
+
expect(() => agent.steer("test")).not.toThrow();
|
|
105
|
+
expect(() => agent.followUp("test")).not.toThrow();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
//# sourceMappingURL=agent-features.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-features.test.js","sourceRoot":"","sources":["../../src/__tests__/agent-features.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/agent.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { Agent } from "../agent.js";
|
|
3
|
+
import { defineTool } from "../define-tool.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
const testModel = { id: "claude-sonnet-4-20250514", name: "Claude Sonnet 4", provider: "anthropic", api: "anthropic-messages" };
|
|
6
|
+
describe("Agent", () => {
|
|
7
|
+
it("should create agent instance", () => {
|
|
8
|
+
const agent = new Agent({
|
|
9
|
+
model: testModel,
|
|
10
|
+
systemPrompt: "You are a helpful assistant.",
|
|
11
|
+
});
|
|
12
|
+
expect(agent).toBeDefined();
|
|
13
|
+
});
|
|
14
|
+
it("should store messages", () => {
|
|
15
|
+
const agent = new Agent({ model: testModel });
|
|
16
|
+
expect(agent.messages).toEqual([]);
|
|
17
|
+
});
|
|
18
|
+
it("should register tools", () => {
|
|
19
|
+
const agent = new Agent({ model: testModel });
|
|
20
|
+
const tool = defineTool({
|
|
21
|
+
name: "get_weather",
|
|
22
|
+
description: "Get the current weather",
|
|
23
|
+
parameters: z.object({
|
|
24
|
+
location: z.string().describe("The location to get weather for"),
|
|
25
|
+
}),
|
|
26
|
+
execute: async (args, context) => {
|
|
27
|
+
return {
|
|
28
|
+
content: [{ type: "text", text: `The weather in ${args.location} is sunny.` }],
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
agent.registerTool(tool);
|
|
33
|
+
expect(agent.messages).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
it("should subscribe and unsubscribe listeners", () => {
|
|
36
|
+
const agent = new Agent({ model: testModel });
|
|
37
|
+
const listener = vi.fn();
|
|
38
|
+
const unsubscribe = agent.subscribe(listener);
|
|
39
|
+
expect(unsubscribe).toBeDefined();
|
|
40
|
+
unsubscribe();
|
|
41
|
+
});
|
|
42
|
+
it("should abort request", () => {
|
|
43
|
+
const agent = new Agent({ model: testModel });
|
|
44
|
+
expect(() => agent.abort()).not.toThrow();
|
|
45
|
+
});
|
|
46
|
+
it("should reset state", () => {
|
|
47
|
+
const agent = new Agent({ model: testModel });
|
|
48
|
+
agent.reset();
|
|
49
|
+
expect(agent.messages).toEqual([]);
|
|
50
|
+
expect(agent.isStreaming).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
it("should return signal", () => {
|
|
53
|
+
const agent = new Agent({ model: testModel });
|
|
54
|
+
const signal = agent.signal;
|
|
55
|
+
expect(signal).toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe("Agent Queues and Hooks", () => {
|
|
59
|
+
it("should add steering messages to queue", () => {
|
|
60
|
+
const agent = new Agent({ model: testModel });
|
|
61
|
+
agent.steer("Please be more concise");
|
|
62
|
+
// Steering message should be queued (internal state)
|
|
63
|
+
// We can't directly check the queue, but we can verify it doesn't throw
|
|
64
|
+
expect(() => agent.steer("Another message")).not.toThrow();
|
|
65
|
+
});
|
|
66
|
+
it("should add follow-up messages to queue", () => {
|
|
67
|
+
const agent = new Agent({ model: testModel });
|
|
68
|
+
agent.followUp("Can you also check the forecast?");
|
|
69
|
+
// Follow-up message should be queued
|
|
70
|
+
expect(() => agent.followUp("And the temperature?")).not.toThrow();
|
|
71
|
+
});
|
|
72
|
+
it("should accept Message objects in steer and followUp", () => {
|
|
73
|
+
const agent = new Agent({ model: testModel });
|
|
74
|
+
const message = {
|
|
75
|
+
role: "user",
|
|
76
|
+
content: "Custom message",
|
|
77
|
+
timestamp: Date.now(),
|
|
78
|
+
};
|
|
79
|
+
expect(() => agent.steer(message)).not.toThrow();
|
|
80
|
+
expect(() => agent.followUp(message)).not.toThrow();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe("Agent Retry Logic", () => {
|
|
84
|
+
it("should configure max retry delay", () => {
|
|
85
|
+
const agent = new Agent({
|
|
86
|
+
model: testModel,
|
|
87
|
+
maxRetryDelayMs: 5000,
|
|
88
|
+
});
|
|
89
|
+
expect(agent).toBeDefined();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe("defineTool", () => {
|
|
93
|
+
it("should create tool with Zod schema", () => {
|
|
94
|
+
const tool = defineTool({
|
|
95
|
+
name: "get_weather",
|
|
96
|
+
description: "Get the current weather",
|
|
97
|
+
parameters: z.object({
|
|
98
|
+
location: z.string().describe("The location"),
|
|
99
|
+
}),
|
|
100
|
+
execute: async (args, context) => ({
|
|
101
|
+
content: [{ type: "text", text: "sunny" }],
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
expect(tool.name).toBe("get_weather");
|
|
105
|
+
expect(tool.description).toBe("Get the current weather");
|
|
106
|
+
expect(tool.parameters).toBeDefined();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
//# sourceMappingURL=agent.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent.test.js","sourceRoot":"","sources":["../../src/__tests__/agent.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"e2e.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/e2e.test.ts"],"names":[],"mappings":""}
|