@jackchen_me/open-multi-agent 0.2.0 → 1.0.1
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 +87 -20
- package/dist/agent/agent.d.ts +15 -1
- package/dist/agent/agent.d.ts.map +1 -1
- package/dist/agent/agent.js +144 -10
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/loop-detector.d.ts +39 -0
- package/dist/agent/loop-detector.d.ts.map +1 -0
- package/dist/agent/loop-detector.js +122 -0
- package/dist/agent/loop-detector.js.map +1 -0
- package/dist/agent/pool.d.ts +2 -1
- package/dist/agent/pool.d.ts.map +1 -1
- package/dist/agent/pool.js +4 -2
- package/dist/agent/pool.js.map +1 -1
- package/dist/agent/runner.d.ts +23 -1
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +113 -12
- package/dist/agent/runner.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/llm/adapter.d.ts +4 -1
- package/dist/llm/adapter.d.ts.map +1 -1
- package/dist/llm/adapter.js +11 -0
- package/dist/llm/adapter.js.map +1 -1
- package/dist/llm/copilot.d.ts.map +1 -1
- package/dist/llm/copilot.js +2 -1
- package/dist/llm/copilot.js.map +1 -1
- package/dist/llm/gemini.d.ts +65 -0
- package/dist/llm/gemini.d.ts.map +1 -0
- package/dist/llm/gemini.js +317 -0
- package/dist/llm/gemini.js.map +1 -0
- package/dist/llm/grok.d.ts +21 -0
- package/dist/llm/grok.d.ts.map +1 -0
- package/dist/llm/grok.js +24 -0
- package/dist/llm/grok.js.map +1 -0
- package/dist/llm/openai-common.d.ts +8 -1
- package/dist/llm/openai-common.d.ts.map +1 -1
- package/dist/llm/openai-common.js +35 -2
- package/dist/llm/openai-common.js.map +1 -1
- package/dist/llm/openai.d.ts +1 -1
- package/dist/llm/openai.d.ts.map +1 -1
- package/dist/llm/openai.js +20 -2
- package/dist/llm/openai.js.map +1 -1
- package/dist/orchestrator/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/orchestrator.js +89 -9
- package/dist/orchestrator/orchestrator.js.map +1 -1
- package/dist/task/queue.d.ts +31 -2
- package/dist/task/queue.d.ts.map +1 -1
- package/dist/task/queue.js +69 -2
- package/dist/task/queue.js.map +1 -1
- package/dist/tool/text-tool-extractor.d.ts +32 -0
- package/dist/tool/text-tool-extractor.d.ts.map +1 -0
- package/dist/tool/text-tool-extractor.js +187 -0
- package/dist/tool/text-tool-extractor.js.map +1 -0
- package/dist/types.d.ts +139 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/trace.d.ts +12 -0
- package/dist/utils/trace.d.ts.map +1 -0
- package/dist/utils/trace.js +30 -0
- package/dist/utils/trace.js.map +1 -0
- package/package.json +18 -2
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -40
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -23
- package/.github/pull_request_template.md +0 -14
- package/.github/workflows/ci.yml +0 -23
- package/CLAUDE.md +0 -72
- package/CODE_OF_CONDUCT.md +0 -48
- package/CONTRIBUTING.md +0 -72
- package/DECISIONS.md +0 -43
- package/README_zh.md +0 -217
- package/SECURITY.md +0 -17
- package/examples/01-single-agent.ts +0 -131
- package/examples/02-team-collaboration.ts +0 -167
- package/examples/03-task-pipeline.ts +0 -201
- package/examples/04-multi-model-team.ts +0 -261
- package/examples/05-copilot-test.ts +0 -49
- package/examples/06-local-model.ts +0 -199
- package/examples/07-fan-out-aggregate.ts +0 -209
- package/examples/08-gemma4-local.ts +0 -203
- package/examples/09-gemma4-auto-orchestration.ts +0 -162
- package/src/agent/agent.ts +0 -473
- package/src/agent/pool.ts +0 -278
- package/src/agent/runner.ts +0 -413
- package/src/agent/structured-output.ts +0 -126
- package/src/index.ts +0 -167
- package/src/llm/adapter.ts +0 -87
- package/src/llm/anthropic.ts +0 -389
- package/src/llm/copilot.ts +0 -551
- package/src/llm/openai-common.ts +0 -255
- package/src/llm/openai.ts +0 -272
- package/src/memory/shared.ts +0 -181
- package/src/memory/store.ts +0 -124
- package/src/orchestrator/orchestrator.ts +0 -977
- package/src/orchestrator/scheduler.ts +0 -352
- package/src/task/queue.ts +0 -394
- package/src/task/task.ts +0 -239
- package/src/team/messaging.ts +0 -232
- package/src/team/team.ts +0 -334
- package/src/tool/built-in/bash.ts +0 -187
- package/src/tool/built-in/file-edit.ts +0 -154
- package/src/tool/built-in/file-read.ts +0 -105
- package/src/tool/built-in/file-write.ts +0 -81
- package/src/tool/built-in/grep.ts +0 -362
- package/src/tool/built-in/index.ts +0 -50
- package/src/tool/executor.ts +0 -178
- package/src/tool/framework.ts +0 -557
- package/src/types.ts +0 -391
- package/src/utils/semaphore.ts +0 -89
- package/tests/semaphore.test.ts +0 -57
- package/tests/shared-memory.test.ts +0 -122
- package/tests/structured-output.test.ts +0 -331
- package/tests/task-queue.test.ts +0 -244
- package/tests/task-retry.test.ts +0 -368
- package/tests/task-utils.test.ts +0 -155
- package/tests/tool-executor.test.ts +0 -193
- package/tsconfig.json +0 -25
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Example 04 — Multi-Model Team with Custom Tools
|
|
3
|
-
*
|
|
4
|
-
* Demonstrates:
|
|
5
|
-
* - Mixing Anthropic and OpenAI models in the same team
|
|
6
|
-
* - Defining custom tools with defineTool() and Zod schemas
|
|
7
|
-
* - Building agents with a custom ToolRegistry so they can use custom tools
|
|
8
|
-
* - Running a team goal that uses the custom tools
|
|
9
|
-
*
|
|
10
|
-
* Run:
|
|
11
|
-
* npx tsx examples/04-multi-model-team.ts
|
|
12
|
-
*
|
|
13
|
-
* Prerequisites:
|
|
14
|
-
* ANTHROPIC_API_KEY and OPENAI_API_KEY env vars must be set.
|
|
15
|
-
* (If you only have one key, set useOpenAI = false below.)
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { z } from 'zod'
|
|
19
|
-
import { OpenMultiAgent, defineTool } from '../src/index.js'
|
|
20
|
-
import type { AgentConfig, OrchestratorEvent } from '../src/types.js'
|
|
21
|
-
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
// Custom tools — defined with defineTool() + Zod schemas
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* A custom tool that fetches live exchange rates from a public API.
|
|
28
|
-
*/
|
|
29
|
-
const exchangeRateTool = defineTool({
|
|
30
|
-
name: 'get_exchange_rate',
|
|
31
|
-
description:
|
|
32
|
-
'Get the current exchange rate between two currencies. ' +
|
|
33
|
-
'Returns the rate as a decimal: 1 unit of `from` = N units of `to`.',
|
|
34
|
-
inputSchema: z.object({
|
|
35
|
-
from: z.string().describe('ISO 4217 currency code, e.g. "USD"'),
|
|
36
|
-
to: z.string().describe('ISO 4217 currency code, e.g. "EUR"'),
|
|
37
|
-
}),
|
|
38
|
-
execute: async ({ from, to }) => {
|
|
39
|
-
try {
|
|
40
|
-
const url = `https://api.exchangerate.host/convert?from=${from}&to=${to}&amount=1`
|
|
41
|
-
const resp = await fetch(url, { signal: AbortSignal.timeout(5000) })
|
|
42
|
-
|
|
43
|
-
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
|
44
|
-
|
|
45
|
-
interface ExchangeRateResponse {
|
|
46
|
-
result?: number
|
|
47
|
-
info?: { rate?: number }
|
|
48
|
-
}
|
|
49
|
-
const json = (await resp.json()) as ExchangeRateResponse
|
|
50
|
-
const rate: number | undefined = json?.result ?? json?.info?.rate
|
|
51
|
-
|
|
52
|
-
if (typeof rate !== 'number') throw new Error('Unexpected API response shape')
|
|
53
|
-
|
|
54
|
-
return {
|
|
55
|
-
data: JSON.stringify({ from, to, rate, timestamp: new Date().toISOString() }),
|
|
56
|
-
isError: false,
|
|
57
|
-
}
|
|
58
|
-
} catch (err) {
|
|
59
|
-
// Graceful degradation — return a stubbed rate so the team can still proceed
|
|
60
|
-
const stub = parseFloat((Math.random() * 0.5 + 0.8).toFixed(4))
|
|
61
|
-
return {
|
|
62
|
-
data: JSON.stringify({
|
|
63
|
-
from,
|
|
64
|
-
to,
|
|
65
|
-
rate: stub,
|
|
66
|
-
note: `Live fetch failed (${err instanceof Error ? err.message : String(err)}). Using stub rate.`,
|
|
67
|
-
}),
|
|
68
|
-
isError: false,
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* A custom tool that formats a number as a localised currency string.
|
|
76
|
-
*/
|
|
77
|
-
const formatCurrencyTool = defineTool({
|
|
78
|
-
name: 'format_currency',
|
|
79
|
-
description: 'Format a number as a localised currency string.',
|
|
80
|
-
inputSchema: z.object({
|
|
81
|
-
amount: z.number().describe('The numeric amount to format.'),
|
|
82
|
-
currency: z.string().describe('ISO 4217 currency code, e.g. "USD".'),
|
|
83
|
-
locale: z
|
|
84
|
-
.string()
|
|
85
|
-
.optional()
|
|
86
|
-
.describe('BCP 47 locale string, e.g. "en-US". Defaults to "en-US".'),
|
|
87
|
-
}),
|
|
88
|
-
execute: async ({ amount, currency, locale = 'en-US' }) => {
|
|
89
|
-
try {
|
|
90
|
-
const formatted = new Intl.NumberFormat(locale, {
|
|
91
|
-
style: 'currency',
|
|
92
|
-
currency,
|
|
93
|
-
}).format(amount)
|
|
94
|
-
return { data: formatted, isError: false }
|
|
95
|
-
} catch {
|
|
96
|
-
return { data: `${amount} ${currency}`, isError: true }
|
|
97
|
-
}
|
|
98
|
-
},
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
// ---------------------------------------------------------------------------
|
|
102
|
-
// Helper: build an AgentConfig whose tools list includes custom tool names.
|
|
103
|
-
//
|
|
104
|
-
// Agents reference tools by name in their AgentConfig.tools array.
|
|
105
|
-
// The ToolRegistry is injected via the Agent constructor. When using OpenMultiAgent
|
|
106
|
-
// convenience methods (runTeam, runTasks, runAgent), the orchestrator builds
|
|
107
|
-
// agents internally using buildAgent(), which registers only the five built-in
|
|
108
|
-
// tools. For custom tools, use AgentPool + Agent directly (see the note in the
|
|
109
|
-
// README) or provide the custom tool names in the tools array and rely on a
|
|
110
|
-
// registry you inject yourself.
|
|
111
|
-
//
|
|
112
|
-
// In this example we demonstrate the custom-tool pattern by running the agents
|
|
113
|
-
// directly through AgentPool rather than through the OpenMultiAgent high-level API.
|
|
114
|
-
// ---------------------------------------------------------------------------
|
|
115
|
-
|
|
116
|
-
import { Agent, AgentPool, ToolRegistry, ToolExecutor, registerBuiltInTools } from '../src/index.js'
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Build an Agent with both built-in and custom tools registered.
|
|
120
|
-
*/
|
|
121
|
-
function buildCustomAgent(
|
|
122
|
-
config: AgentConfig,
|
|
123
|
-
extraTools: ReturnType<typeof defineTool>[],
|
|
124
|
-
): Agent {
|
|
125
|
-
const registry = new ToolRegistry()
|
|
126
|
-
registerBuiltInTools(registry)
|
|
127
|
-
for (const tool of extraTools) {
|
|
128
|
-
registry.register(tool)
|
|
129
|
-
}
|
|
130
|
-
const executor = new ToolExecutor(registry)
|
|
131
|
-
return new Agent(config, registry, executor)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// ---------------------------------------------------------------------------
|
|
135
|
-
// Agent definitions — mixed providers
|
|
136
|
-
// ---------------------------------------------------------------------------
|
|
137
|
-
|
|
138
|
-
const useOpenAI = Boolean(process.env.OPENAI_API_KEY)
|
|
139
|
-
|
|
140
|
-
const researcherConfig: AgentConfig = {
|
|
141
|
-
name: 'researcher',
|
|
142
|
-
model: 'claude-sonnet-4-6',
|
|
143
|
-
provider: 'anthropic',
|
|
144
|
-
systemPrompt: `You are a financial data researcher.
|
|
145
|
-
Use the get_exchange_rate tool to fetch current rates between the currency pairs you are given.
|
|
146
|
-
Return the raw rates as a JSON object keyed by pair, e.g. { "USD/EUR": 0.91, "USD/GBP": 0.79 }.`,
|
|
147
|
-
tools: ['get_exchange_rate'],
|
|
148
|
-
maxTurns: 6,
|
|
149
|
-
temperature: 0,
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const analystConfig: AgentConfig = {
|
|
153
|
-
name: 'analyst',
|
|
154
|
-
model: useOpenAI ? 'gpt-5.4' : 'claude-sonnet-4-6',
|
|
155
|
-
provider: useOpenAI ? 'openai' : 'anthropic',
|
|
156
|
-
systemPrompt: `You are a foreign exchange analyst.
|
|
157
|
-
You receive exchange rate data and produce a short briefing.
|
|
158
|
-
Use format_currency to show example conversions.
|
|
159
|
-
Keep the briefing under 200 words.`,
|
|
160
|
-
tools: ['format_currency'],
|
|
161
|
-
maxTurns: 4,
|
|
162
|
-
temperature: 0.3,
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// ---------------------------------------------------------------------------
|
|
166
|
-
// Build agents with custom tools
|
|
167
|
-
// ---------------------------------------------------------------------------
|
|
168
|
-
|
|
169
|
-
const researcher = buildCustomAgent(researcherConfig, [exchangeRateTool])
|
|
170
|
-
const analyst = buildCustomAgent(analystConfig, [formatCurrencyTool])
|
|
171
|
-
|
|
172
|
-
// ---------------------------------------------------------------------------
|
|
173
|
-
// Run with AgentPool for concurrency control
|
|
174
|
-
// ---------------------------------------------------------------------------
|
|
175
|
-
|
|
176
|
-
console.log('Multi-model team with custom tools')
|
|
177
|
-
console.log(`Providers: researcher=anthropic, analyst=${useOpenAI ? 'openai (gpt-5.4)' : 'anthropic (fallback)'}`)
|
|
178
|
-
console.log('Custom tools:', [exchangeRateTool.name, formatCurrencyTool.name].join(', '))
|
|
179
|
-
console.log()
|
|
180
|
-
|
|
181
|
-
const pool = new AgentPool(1) // sequential for readability
|
|
182
|
-
pool.add(researcher)
|
|
183
|
-
pool.add(analyst)
|
|
184
|
-
|
|
185
|
-
// Step 1: researcher fetches the rates
|
|
186
|
-
console.log('[1/2] Researcher fetching FX rates...')
|
|
187
|
-
const researchResult = await pool.run(
|
|
188
|
-
'researcher',
|
|
189
|
-
`Fetch exchange rates for these pairs using the get_exchange_rate tool:
|
|
190
|
-
- USD to EUR
|
|
191
|
-
- USD to GBP
|
|
192
|
-
- USD to JPY
|
|
193
|
-
- EUR to GBP
|
|
194
|
-
|
|
195
|
-
Return the results as a JSON object: { "USD/EUR": <rate>, "USD/GBP": <rate>, ... }`,
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
if (!researchResult.success) {
|
|
199
|
-
console.error('Researcher failed:', researchResult.output)
|
|
200
|
-
process.exit(1)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
console.log('Researcher done. Tool calls made:', researchResult.toolCalls.map(c => c.toolName).join(', '))
|
|
204
|
-
|
|
205
|
-
// Step 2: analyst writes the briefing, receiving the researcher output as context
|
|
206
|
-
console.log('\n[2/2] Analyst writing FX briefing...')
|
|
207
|
-
const analystResult = await pool.run(
|
|
208
|
-
'analyst',
|
|
209
|
-
`Here are the current FX rates gathered by the research team:
|
|
210
|
-
|
|
211
|
-
${researchResult.output}
|
|
212
|
-
|
|
213
|
-
Using format_currency, show what $1,000 USD and €1,000 EUR convert to in each of the other currencies.
|
|
214
|
-
Then write a short FX market briefing (under 200 words) covering:
|
|
215
|
-
- Each rate with a brief observation
|
|
216
|
-
- The strongest and weakest currency in the set
|
|
217
|
-
- One-sentence market comment`,
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
if (!analystResult.success) {
|
|
221
|
-
console.error('Analyst failed:', analystResult.output)
|
|
222
|
-
process.exit(1)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
console.log('Analyst done. Tool calls made:', analystResult.toolCalls.map(c => c.toolName).join(', '))
|
|
226
|
-
|
|
227
|
-
// ---------------------------------------------------------------------------
|
|
228
|
-
// Results
|
|
229
|
-
// ---------------------------------------------------------------------------
|
|
230
|
-
|
|
231
|
-
console.log('\n' + '='.repeat(60))
|
|
232
|
-
|
|
233
|
-
console.log('\nResearcher output:')
|
|
234
|
-
console.log(researchResult.output.slice(0, 400))
|
|
235
|
-
|
|
236
|
-
console.log('\nAnalyst briefing:')
|
|
237
|
-
console.log('─'.repeat(60))
|
|
238
|
-
console.log(analystResult.output)
|
|
239
|
-
console.log('─'.repeat(60))
|
|
240
|
-
|
|
241
|
-
const totalInput = researchResult.tokenUsage.input_tokens + analystResult.tokenUsage.input_tokens
|
|
242
|
-
const totalOutput = researchResult.tokenUsage.output_tokens + analystResult.tokenUsage.output_tokens
|
|
243
|
-
console.log(`\nTotal tokens — input: ${totalInput}, output: ${totalOutput}`)
|
|
244
|
-
|
|
245
|
-
// ---------------------------------------------------------------------------
|
|
246
|
-
// Bonus: show how defineTool() works in isolation (no LLM needed)
|
|
247
|
-
// ---------------------------------------------------------------------------
|
|
248
|
-
|
|
249
|
-
console.log('\n--- Bonus: testing custom tools in isolation ---\n')
|
|
250
|
-
|
|
251
|
-
const fmtResult = await formatCurrencyTool.execute(
|
|
252
|
-
{ amount: 1234.56, currency: 'EUR', locale: 'de-DE' },
|
|
253
|
-
{ agent: { name: 'test', role: 'test', model: 'test' } },
|
|
254
|
-
)
|
|
255
|
-
console.log(`format_currency(1234.56, EUR, de-DE) = ${fmtResult.data}`)
|
|
256
|
-
|
|
257
|
-
const rateResult = await exchangeRateTool.execute(
|
|
258
|
-
{ from: 'USD', to: 'EUR' },
|
|
259
|
-
{ agent: { name: 'test', role: 'test', model: 'test' } },
|
|
260
|
-
)
|
|
261
|
-
console.log(`get_exchange_rate(USD→EUR) = ${rateResult.data}`)
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Quick smoke test for the Copilot adapter.
|
|
3
|
-
*
|
|
4
|
-
* Run:
|
|
5
|
-
* npx tsx examples/05-copilot-test.ts
|
|
6
|
-
*
|
|
7
|
-
* If GITHUB_COPILOT_TOKEN is not set, the adapter will start an interactive
|
|
8
|
-
* OAuth2 device flow — you'll be prompted to sign in via your browser.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { OpenMultiAgent } from '../src/index.js'
|
|
12
|
-
import type { OrchestratorEvent } from '../src/types.js'
|
|
13
|
-
|
|
14
|
-
const orchestrator = new OpenMultiAgent({
|
|
15
|
-
defaultModel: 'gpt-4o',
|
|
16
|
-
defaultProvider: 'copilot',
|
|
17
|
-
onProgress: (event: OrchestratorEvent) => {
|
|
18
|
-
if (event.type === 'agent_start') {
|
|
19
|
-
console.log(`[start] agent=${event.agent}`)
|
|
20
|
-
} else if (event.type === 'agent_complete') {
|
|
21
|
-
console.log(`[complete] agent=${event.agent}`)
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
console.log('Testing Copilot adapter with gpt-4o...\n')
|
|
27
|
-
|
|
28
|
-
const result = await orchestrator.runAgent(
|
|
29
|
-
{
|
|
30
|
-
name: 'assistant',
|
|
31
|
-
model: 'gpt-4o',
|
|
32
|
-
provider: 'copilot',
|
|
33
|
-
systemPrompt: 'You are a helpful assistant. Keep answers brief.',
|
|
34
|
-
maxTurns: 1,
|
|
35
|
-
maxTokens: 256,
|
|
36
|
-
},
|
|
37
|
-
'What is 2 + 2? Reply in one sentence.',
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
if (result.success) {
|
|
41
|
-
console.log('\nAgent output:')
|
|
42
|
-
console.log('─'.repeat(60))
|
|
43
|
-
console.log(result.output)
|
|
44
|
-
console.log('─'.repeat(60))
|
|
45
|
-
console.log(`\nTokens: input=${result.tokenUsage.input_tokens}, output=${result.tokenUsage.output_tokens}`)
|
|
46
|
-
} else {
|
|
47
|
-
console.error('Agent failed:', result.output)
|
|
48
|
-
process.exit(1)
|
|
49
|
-
}
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Example 06 — Local Model + Cloud Model Team (Ollama + Claude)
|
|
3
|
-
*
|
|
4
|
-
* Demonstrates mixing a local model served by Ollama with a cloud model
|
|
5
|
-
* (Claude) in the same task pipeline. The key technique is using
|
|
6
|
-
* `provider: 'openai'` with a custom `baseURL` pointing at Ollama's
|
|
7
|
-
* OpenAI-compatible endpoint.
|
|
8
|
-
*
|
|
9
|
-
* This pattern works with ANY OpenAI-compatible local server:
|
|
10
|
-
* - Ollama → http://localhost:11434/v1
|
|
11
|
-
* - vLLM → http://localhost:8000/v1
|
|
12
|
-
* - LM Studio → http://localhost:1234/v1
|
|
13
|
-
* - llama.cpp → http://localhost:8080/v1
|
|
14
|
-
* Just change the baseURL and model name below.
|
|
15
|
-
*
|
|
16
|
-
* Run:
|
|
17
|
-
* npx tsx examples/06-local-model.ts
|
|
18
|
-
*
|
|
19
|
-
* Prerequisites:
|
|
20
|
-
* 1. Ollama installed and running: https://ollama.com
|
|
21
|
-
* 2. Pull the model: ollama pull llama3.1
|
|
22
|
-
* 3. ANTHROPIC_API_KEY env var must be set.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import { OpenMultiAgent } from '../src/index.js'
|
|
26
|
-
import type { AgentConfig, OrchestratorEvent, Task } from '../src/types.js'
|
|
27
|
-
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
// Agents
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Coder — uses Claude (Anthropic) for high-quality code generation.
|
|
34
|
-
*/
|
|
35
|
-
const coder: AgentConfig = {
|
|
36
|
-
name: 'coder',
|
|
37
|
-
model: 'claude-sonnet-4-6',
|
|
38
|
-
provider: 'anthropic',
|
|
39
|
-
systemPrompt: `You are a senior TypeScript developer. Write clean, well-typed,
|
|
40
|
-
production-quality code. Use the tools to write files to /tmp/local-model-demo/.
|
|
41
|
-
Always include brief JSDoc comments on exported functions.`,
|
|
42
|
-
tools: ['bash', 'file_write'],
|
|
43
|
-
maxTurns: 6,
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Reviewer — uses a local Ollama model via the OpenAI-compatible API.
|
|
48
|
-
* The apiKey is required by the OpenAI SDK but Ollama ignores it,
|
|
49
|
-
* so we pass the placeholder string 'ollama'.
|
|
50
|
-
*/
|
|
51
|
-
const reviewer: AgentConfig = {
|
|
52
|
-
name: 'reviewer',
|
|
53
|
-
model: 'llama3.1',
|
|
54
|
-
provider: 'openai', // 'openai' here means "OpenAI-compatible protocol", not the OpenAI cloud
|
|
55
|
-
baseURL: 'http://localhost:11434/v1',
|
|
56
|
-
apiKey: 'ollama',
|
|
57
|
-
systemPrompt: `You are a code reviewer. You read source files and produce a structured review.
|
|
58
|
-
Your review MUST include these sections:
|
|
59
|
-
- Summary (2-3 sentences)
|
|
60
|
-
- Strengths (bullet list)
|
|
61
|
-
- Issues (bullet list — or "None found" if the code is clean)
|
|
62
|
-
- Verdict: SHIP or NEEDS WORK
|
|
63
|
-
|
|
64
|
-
Be specific and constructive. Reference line numbers or function names when possible.`,
|
|
65
|
-
tools: ['file_read'],
|
|
66
|
-
maxTurns: 4,
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
// Progress handler
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
|
|
73
|
-
const taskTimes = new Map<string, number>()
|
|
74
|
-
|
|
75
|
-
function handleProgress(event: OrchestratorEvent): void {
|
|
76
|
-
const ts = new Date().toISOString().slice(11, 23)
|
|
77
|
-
|
|
78
|
-
switch (event.type) {
|
|
79
|
-
case 'task_start': {
|
|
80
|
-
taskTimes.set(event.task ?? '', Date.now())
|
|
81
|
-
const task = event.data as Task | undefined
|
|
82
|
-
console.log(`[${ts}] TASK READY "${task?.title ?? event.task}" → ${task?.assignee ?? '?'}`)
|
|
83
|
-
break
|
|
84
|
-
}
|
|
85
|
-
case 'task_complete': {
|
|
86
|
-
const elapsed = Date.now() - (taskTimes.get(event.task ?? '') ?? Date.now())
|
|
87
|
-
console.log(`[${ts}] TASK DONE task=${event.task} in ${elapsed}ms`)
|
|
88
|
-
break
|
|
89
|
-
}
|
|
90
|
-
case 'agent_start':
|
|
91
|
-
console.log(`[${ts}] AGENT START ${event.agent}`)
|
|
92
|
-
break
|
|
93
|
-
case 'agent_complete':
|
|
94
|
-
console.log(`[${ts}] AGENT DONE ${event.agent}`)
|
|
95
|
-
break
|
|
96
|
-
case 'error':
|
|
97
|
-
console.error(`[${ts}] ERROR ${event.agent ?? ''} task=${event.task ?? '?'}`)
|
|
98
|
-
break
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ---------------------------------------------------------------------------
|
|
103
|
-
// Orchestrator + Team
|
|
104
|
-
// ---------------------------------------------------------------------------
|
|
105
|
-
|
|
106
|
-
const orchestrator = new OpenMultiAgent({
|
|
107
|
-
defaultModel: 'claude-sonnet-4-6',
|
|
108
|
-
maxConcurrency: 2,
|
|
109
|
-
onProgress: handleProgress,
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
const team = orchestrator.createTeam('local-cloud-team', {
|
|
113
|
-
name: 'local-cloud-team',
|
|
114
|
-
agents: [coder, reviewer],
|
|
115
|
-
sharedMemory: true,
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
// ---------------------------------------------------------------------------
|
|
119
|
-
// Task pipeline: code → review
|
|
120
|
-
// ---------------------------------------------------------------------------
|
|
121
|
-
|
|
122
|
-
const OUTPUT_DIR = '/tmp/local-model-demo'
|
|
123
|
-
|
|
124
|
-
const tasks: Array<{
|
|
125
|
-
title: string
|
|
126
|
-
description: string
|
|
127
|
-
assignee?: string
|
|
128
|
-
dependsOn?: string[]
|
|
129
|
-
}> = [
|
|
130
|
-
{
|
|
131
|
-
title: 'Write: retry utility',
|
|
132
|
-
description: `Write a small but complete TypeScript utility to ${OUTPUT_DIR}/retry.ts.
|
|
133
|
-
|
|
134
|
-
The module should export:
|
|
135
|
-
1. A \`RetryOptions\` interface with: maxRetries (number), delayMs (number),
|
|
136
|
-
backoffFactor (optional number, default 2), shouldRetry (optional predicate
|
|
137
|
-
taking the error and returning boolean).
|
|
138
|
-
2. An async \`retry<T>(fn: () => Promise<T>, options: RetryOptions): Promise<T>\`
|
|
139
|
-
function that retries \`fn\` with exponential backoff.
|
|
140
|
-
3. A convenience \`withRetry\` wrapper that returns a new function with retry
|
|
141
|
-
behaviour baked in.
|
|
142
|
-
|
|
143
|
-
Include JSDoc comments. No external dependencies — use only Node built-ins.
|
|
144
|
-
After writing the file, also create a small test script at ${OUTPUT_DIR}/retry-test.ts
|
|
145
|
-
that exercises the happy path and a failure case, then run it with \`npx tsx\`.`,
|
|
146
|
-
assignee: 'coder',
|
|
147
|
-
},
|
|
148
|
-
{
|
|
149
|
-
title: 'Review: retry utility',
|
|
150
|
-
description: `Read the files at ${OUTPUT_DIR}/retry.ts and ${OUTPUT_DIR}/retry-test.ts.
|
|
151
|
-
|
|
152
|
-
Produce a structured code review covering:
|
|
153
|
-
- Summary (2-3 sentences describing the module)
|
|
154
|
-
- Strengths (bullet list)
|
|
155
|
-
- Issues (bullet list — be specific about what and why)
|
|
156
|
-
- Verdict: SHIP or NEEDS WORK`,
|
|
157
|
-
assignee: 'reviewer',
|
|
158
|
-
dependsOn: ['Write: retry utility'],
|
|
159
|
-
},
|
|
160
|
-
]
|
|
161
|
-
|
|
162
|
-
// ---------------------------------------------------------------------------
|
|
163
|
-
// Run
|
|
164
|
-
// ---------------------------------------------------------------------------
|
|
165
|
-
|
|
166
|
-
console.log('Local + Cloud model team')
|
|
167
|
-
console.log(` coder → Claude (${coder.model}) via Anthropic API`)
|
|
168
|
-
console.log(` reviewer → Ollama (${reviewer.model}) at ${reviewer.baseURL}`)
|
|
169
|
-
console.log()
|
|
170
|
-
console.log('Pipeline: coder writes code → local model reviews it')
|
|
171
|
-
console.log('='.repeat(60))
|
|
172
|
-
|
|
173
|
-
const result = await orchestrator.runTasks(team, tasks)
|
|
174
|
-
|
|
175
|
-
// ---------------------------------------------------------------------------
|
|
176
|
-
// Summary
|
|
177
|
-
// ---------------------------------------------------------------------------
|
|
178
|
-
|
|
179
|
-
console.log('\n' + '='.repeat(60))
|
|
180
|
-
console.log('Pipeline complete.\n')
|
|
181
|
-
console.log(`Overall success: ${result.success}`)
|
|
182
|
-
console.log(`Tokens — input: ${result.totalTokenUsage.input_tokens}, output: ${result.totalTokenUsage.output_tokens}`)
|
|
183
|
-
|
|
184
|
-
console.log('\nPer-agent summary:')
|
|
185
|
-
for (const [name, r] of result.agentResults) {
|
|
186
|
-
const icon = r.success ? 'OK ' : 'FAIL'
|
|
187
|
-
const provider = name === 'coder' ? 'anthropic' : 'ollama (local)'
|
|
188
|
-
const tools = r.toolCalls.map(c => c.toolName).join(', ')
|
|
189
|
-
console.log(` [${icon}] ${name.padEnd(10)} (${provider.padEnd(16)}) tools: ${tools || '(none)'}`)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Print the reviewer's output
|
|
193
|
-
const review = result.agentResults.get('reviewer')
|
|
194
|
-
if (review?.success) {
|
|
195
|
-
console.log('\nCode review (from local model):')
|
|
196
|
-
console.log('─'.repeat(60))
|
|
197
|
-
console.log(review.output)
|
|
198
|
-
console.log('─'.repeat(60))
|
|
199
|
-
}
|