loopengine 0.0.4 → 0.0.6
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 -13
- package/dist/deepseek-model-call.d.ts +15 -0
- package/dist/deepseek-model-call.js +53 -0
- package/dist/discover-agents.d.ts +16 -0
- package/dist/discover-agents.js +48 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/openai-model-call.d.ts +6 -1
- package/dist/openai-model-call.js +30 -21
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -74,17 +74,18 @@ console.log(result.text)
|
|
|
74
74
|
|
|
75
75
|
The full exported surface: `runAgent`, `AgentConfig`/`ToolSchema`/`ToolDefinition`,
|
|
76
76
|
`FileSessionStore`/`RedisSessionStore`/`createSessionStore`, `VectorIndex`/
|
|
77
|
-
`embed`/`cosineSimilarity` (for RAG — see below),
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
`embed`/`cosineSimilarity` (for RAG — see below), `discoverAgents` (see
|
|
78
|
+
"Auto-discovered agent registry" below), and `createAnthropicModelCall`/
|
|
79
|
+
`createOpenAIModelCall`/`createDeepSeekModelCall` (real, ready-to-use
|
|
80
|
+
`ModelCall`s). See `index.ts` for the exact list.
|
|
80
81
|
|
|
81
82
|
**Not** part of the package: `agents/*.ts` (this repo's own demo agents),
|
|
82
|
-
`agent-registry.ts` (this repo's own name → config lookup table
|
|
83
|
-
own
|
|
84
|
-
(one specific CLI/HTTP wiring choice, not imposed on
|
|
85
|
-
the `Dockerfile`. Everything below this point
|
|
86
|
-
read it as a worked example of what to
|
|
87
|
-
library API itself.
|
|
83
|
+
`agent-registry.ts` (this repo's own name → config lookup table, built on
|
|
84
|
+
`discoverAgents` — build your own the same way), `adapters/cli.ts`/
|
|
85
|
+
`adapters/http.ts` (one specific CLI/HTTP wiring choice, not imposed on
|
|
86
|
+
every consumer), and the `Dockerfile`. Everything below this point
|
|
87
|
+
describes that reference app — read it as a worked example of what to
|
|
88
|
+
build with the library, not as library API itself.
|
|
88
89
|
|
|
89
90
|
## Quick start
|
|
90
91
|
|
|
@@ -130,9 +131,47 @@ export const config: AgentConfig = {
|
|
|
130
131
|
`agents/file-agent.ts` and `agents/customer-service-agent.ts` are two complete, working
|
|
131
132
|
examples — same `runAgent` loop, entirely different personas and tools.
|
|
132
133
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
A file exporting `config` and `createModelCall` and saved into `agents/` is
|
|
135
|
+
runnable through both adapters immediately — `agent-registry.ts` discovers
|
|
136
|
+
it automatically (by `AgentConfig.name`, not the filename) at startup.
|
|
137
|
+
Nothing to edit, no import to add, no adapter change. See "Auto-discovered
|
|
138
|
+
agent registry" below for how and why.
|
|
139
|
+
|
|
140
|
+
## Auto-discovered agent registry
|
|
141
|
+
|
|
142
|
+
`discoverAgents(dir)` — exported from the library, used by this repo's own
|
|
143
|
+
`agent-registry.ts` — scans a directory's direct files (not
|
|
144
|
+
subdirectories, so shared helper code has somewhere to live without being
|
|
145
|
+
mistaken for an agent) for modules exporting `config`/`createModelCall`,
|
|
146
|
+
and builds a `Map<name, {config, createModelCall}>` from them:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import { dirname, join } from 'node:path'
|
|
150
|
+
import { fileURLToPath } from 'node:url'
|
|
151
|
+
import { discoverAgents } from 'loopengine'
|
|
152
|
+
|
|
153
|
+
const agentsDir = join(dirname(fileURLToPath(import.meta.url)), 'agents')
|
|
154
|
+
const entries = await discoverAgents(agentsDir) // top-level await — fine in ESM
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Keyed by `AgentConfig.name`, not the filename — that's the identity
|
|
158
|
+
ActAuth's `scope.agent` segment already uses, so this doesn't introduce a
|
|
159
|
+
second naming scheme, and a rename is just editing `config.name`, not also
|
|
160
|
+
a registry line and an import path. Two files that declared the same
|
|
161
|
+
`config.name` would already silently collide in ActAuth's rules before
|
|
162
|
+
this existed; `discoverAgents` throws loudly on that (and on a file
|
|
163
|
+
missing either export) at startup instead, which is a real safety
|
|
164
|
+
property this adds, not just less typing.
|
|
165
|
+
|
|
166
|
+
`agent-registry.ts` is a ~15-line wrapper around this: resolve `agents/`
|
|
167
|
+
relative to its own file, call `discoverAgents` once at module load behind
|
|
168
|
+
a top-level `await`, and expose `listAgents()`/`getEntry(name)` as
|
|
169
|
+
synchronous functions from the result — ESM guarantees an importer's own
|
|
170
|
+
evaluation (`adapters/http.ts`, `adapters/cli.ts`) waits for that
|
|
171
|
+
top-level await to settle first, so neither adapter needs to know
|
|
172
|
+
discovery is async underneath. Verified live: dropped a new agent file
|
|
173
|
+
into `agents/` with zero other changes and called it immediately through
|
|
174
|
+
both `adapters/cli.ts` and `adapters/http.ts`.
|
|
136
175
|
|
|
137
176
|
## Retrieval (RAG)
|
|
138
177
|
|
|
@@ -385,6 +424,18 @@ const modelCall = createOpenAIModelCall({ model: 'gpt-4.1' })
|
|
|
385
424
|
const result = await runAgent(config, modelCall, 'order A-1001 arrived broken', [])
|
|
386
425
|
```
|
|
387
426
|
|
|
427
|
+
```ts
|
|
428
|
+
import { createDeepSeekModelCall } from './deepseek-model-call.js'
|
|
429
|
+
|
|
430
|
+
// model is required, same reasoning as OpenAI's; reads DEEPSEEK_API_KEY
|
|
431
|
+
// from the env — throws immediately, with a DeepSeek-specific message, if
|
|
432
|
+
// neither that nor apiKey is set, rather than the OpenAI SDK's own
|
|
433
|
+
// constructor silently accepting OPENAI_API_KEY instead.
|
|
434
|
+
const modelCall = createDeepSeekModelCall({ model: 'deepseek-chat' })
|
|
435
|
+
|
|
436
|
+
const result = await runAgent(config, modelCall, 'order A-1001 arrived broken', [])
|
|
437
|
+
```
|
|
438
|
+
|
|
388
439
|
Nothing else in `run-agent.ts`, the adapters, or any `AgentConfig` needs to
|
|
389
440
|
change — `ModelCall` is the only seam a real API call needs.
|
|
390
441
|
|
|
@@ -410,6 +461,15 @@ user-role message (mirroring Anthropic's shape); OpenAI has no equivalent —
|
|
|
410
461
|
each becomes its own top-level `role: 'tool'` message, so one loopengine
|
|
411
462
|
`Message` can expand into several OpenAI messages, never the reverse.
|
|
412
463
|
|
|
464
|
+
`deepseek-model-call.ts` reuses `openai-model-call.ts`'s translation
|
|
465
|
+
verbatim (exported, not duplicated) rather than reimplementing it —
|
|
466
|
+
DeepSeek's Chat Completions API is wire-compatible with OpenAI's (verified
|
|
467
|
+
against DeepSeek's own docs), down to the same `tool_calls` response
|
|
468
|
+
shape. The one real difference: DeepSeek documents `max_tokens`, not the
|
|
469
|
+
newer `max_completion_tokens` OpenAI's endpoint expects, so only the
|
|
470
|
+
request-building call site differs, not the shared message/response
|
|
471
|
+
translation functions.
|
|
472
|
+
|
|
413
473
|
## Skills
|
|
414
474
|
|
|
415
475
|
Agents can discover and invoke `SKILL.md` files (see `skills/`), the same
|
|
@@ -446,10 +506,12 @@ index.ts Public API surface — what actually ships to npm
|
|
|
446
506
|
agent-config.ts AgentConfig type — the thing you fill in to define an agent
|
|
447
507
|
run-agent.ts The generic ReAct loop every agent and adapter runs through
|
|
448
508
|
vector-index.ts Dependency-free embeddings + cosine-similarity search, for RAG
|
|
449
|
-
|
|
509
|
+
discover-agents.ts discoverAgents — scans a directory, builds {name -> {config, createModelCall}}
|
|
510
|
+
agent-registry.ts This repo's ~15-line wrapper: discoverAgents('agents/') + listAgents()/getEntry()
|
|
450
511
|
session-store.ts SessionStore: FileSessionStore, RedisSessionStore, createSessionStore()
|
|
451
512
|
anthropic-model-call.ts createAnthropicModelCall — a real ModelCall this repo ships
|
|
452
513
|
openai-model-call.ts createOpenAIModelCall — same seam, OpenAI's Chat Completions API
|
|
514
|
+
deepseek-model-call.ts createDeepSeekModelCall — same seam, reuses openai-model-call.ts's translation
|
|
453
515
|
agents/file-agent.ts Example agent: summarizes text files
|
|
454
516
|
agents/customer-service-agent.ts Example agent: order/shipment lookup, refund, email
|
|
455
517
|
agents/rag-agent.ts Example agent: retrieves from an in-memory vector index
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import type { ModelCall } from './run-agent.js';
|
|
3
|
+
export interface DeepSeekModelCallOptions {
|
|
4
|
+
/** Defaults to the DEEPSEEK_API_KEY env var. */
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
/** No hardcoded default — same reasoning as OpenAIModelCallOptions.model:
|
|
7
|
+
* pass the one you actually want. */
|
|
8
|
+
model: string;
|
|
9
|
+
maxTokens?: number;
|
|
10
|
+
/** Defaults to DeepSeek's own API — override to point at a proxy/mirror. */
|
|
11
|
+
baseURL?: string;
|
|
12
|
+
/** Inject a pre-configured client instead of building one from apiKey/baseURL — e.g. to pass a custom `fetch` in tests. */
|
|
13
|
+
client?: OpenAI;
|
|
14
|
+
}
|
|
15
|
+
export declare function createDeepSeekModelCall(options: DeepSeekModelCallOptions): ModelCall;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// A third real ModelCall implementation. DeepSeek's Chat Completions API
|
|
2
|
+
// is wire-compatible with OpenAI's — confirmed against DeepSeek's own docs
|
|
3
|
+
// (api-docs.deepseek.com/api/create-chat-completion/): same messages/
|
|
4
|
+
// tools/tool_choice request shape, same tool_calls response shape — so
|
|
5
|
+
// this reuses openai-model-call.ts's message/response translation
|
|
6
|
+
// verbatim via the real `openai` SDK client, just pointed at DeepSeek's
|
|
7
|
+
// base URL, rather than duplicating that logic.
|
|
8
|
+
//
|
|
9
|
+
// The one real difference: DeepSeek's docs list `max_tokens` as the
|
|
10
|
+
// token-limit field, not the newer `max_completion_tokens`
|
|
11
|
+
// openai-model-call.ts uses (OpenAI deprecated max_tokens in favor of
|
|
12
|
+
// max_completion_tokens; DeepSeek's endpoint doesn't document the newer
|
|
13
|
+
// field at all) — decided at the request-building call site below, since
|
|
14
|
+
// toMessageParams/toModelResponse don't touch that field either way.
|
|
15
|
+
import OpenAI from 'openai';
|
|
16
|
+
import { toMessageParams, toModelResponse } from './openai-model-call.js';
|
|
17
|
+
export function createDeepSeekModelCall(options) {
|
|
18
|
+
if (!options.client && !options.apiKey && !process.env.DEEPSEEK_API_KEY) {
|
|
19
|
+
// Fails here, with a DeepSeek-specific message, rather than letting
|
|
20
|
+
// the OpenAI SDK's own constructor validation reject it — that error
|
|
21
|
+
// says "set OPENAI_API_KEY", which would send someone who never
|
|
22
|
+
// touched that variable chasing the wrong fix.
|
|
23
|
+
throw new Error('createDeepSeekModelCall: no apiKey given and DEEPSEEK_API_KEY is not set.');
|
|
24
|
+
}
|
|
25
|
+
const client = options.client ??
|
|
26
|
+
new OpenAI({
|
|
27
|
+
// Explicitly null, not undefined, if neither is set — the OpenAI
|
|
28
|
+
// SDK's own constructor falls back to reading OPENAI_API_KEY itself
|
|
29
|
+
// when apiKey is undefined (its normal, correct behavior for
|
|
30
|
+
// createOpenAIModelCall), which here would silently authenticate
|
|
31
|
+
// against DeepSeek with an OpenAI key if one happens to be set. The
|
|
32
|
+
// guard above already rules out reaching here with no key at all,
|
|
33
|
+
// but null (not undefined) is kept regardless so this line is
|
|
34
|
+
// correct on its own, not just because of the guard before it.
|
|
35
|
+
apiKey: options.apiKey ?? process.env.DEEPSEEK_API_KEY ?? null,
|
|
36
|
+
baseURL: options.baseURL ?? 'https://api.deepseek.com',
|
|
37
|
+
});
|
|
38
|
+
const maxTokens = options.maxTokens ?? 4096;
|
|
39
|
+
return async (messages, system, tools) => {
|
|
40
|
+
const response = await client.chat.completions.create({
|
|
41
|
+
model: options.model,
|
|
42
|
+
max_tokens: maxTokens,
|
|
43
|
+
messages: [{ role: 'system', content: system }, ...messages.flatMap(toMessageParams)],
|
|
44
|
+
tools: tools.length
|
|
45
|
+
? tools.map((t) => ({
|
|
46
|
+
type: 'function',
|
|
47
|
+
function: { name: t.name, description: t.description, parameters: t.input_schema },
|
|
48
|
+
}))
|
|
49
|
+
: undefined,
|
|
50
|
+
});
|
|
51
|
+
return toModelResponse(response.choices[0]);
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AgentConfig } from './agent-config.js';
|
|
2
|
+
import type { ModelCall } from './run-agent.js';
|
|
3
|
+
export interface AgentModule {
|
|
4
|
+
config: AgentConfig;
|
|
5
|
+
/** Factory, not a shared instance — a fresh ModelCall per request/session. */
|
|
6
|
+
createModelCall: () => ModelCall;
|
|
7
|
+
}
|
|
8
|
+
/** Scans `dir`'s direct files only (not subdirectories — put shared,
|
|
9
|
+
* non-agent code in a subdirectory to keep it out of discovery, no
|
|
10
|
+
* separate exclude-list mechanism needed) for `.ts`/`.js` modules, each
|
|
11
|
+
* expected to export `config` and `createModelCall` — the same shape
|
|
12
|
+
* every agents/*.ts file in this repo's own reference app already
|
|
13
|
+
* exports. Throws, rather than skipping, on a file that doesn't match
|
|
14
|
+
* that shape or a duplicate AgentConfig.name — both are almost always a
|
|
15
|
+
* bug in the agent file, not something to silently ignore. */
|
|
16
|
+
export declare function discoverAgents(dir: string): Promise<Map<string, AgentModule>>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Scans a directory for agent modules and builds a name -> module map from
|
|
2
|
+
// it — the thing that makes "drop a new file in agents/" enough to
|
|
3
|
+
// register an agent, no hand-written import-and-add-one-line edit needed
|
|
4
|
+
// anywhere else. See agent-registry.ts for the one place this repo's own
|
|
5
|
+
// reference app actually calls it.
|
|
6
|
+
//
|
|
7
|
+
// Keyed by AgentConfig.name, not the filename — that's the same identity
|
|
8
|
+
// ActAuth's scope.agent segment already uses (see AgentConfig.name's own
|
|
9
|
+
// doc comment), so this doesn't introduce a second naming scheme. Two
|
|
10
|
+
// files that happened to declare the same config.name would already
|
|
11
|
+
// silently collide in ActAuth's rules today regardless of this helper;
|
|
12
|
+
// surfacing that collision loudly here, at discovery time, is a real
|
|
13
|
+
// safety property this adds, not just convenience.
|
|
14
|
+
import { readdirSync } from 'node:fs';
|
|
15
|
+
import { resolve } from 'node:path';
|
|
16
|
+
import { pathToFileURL } from 'node:url';
|
|
17
|
+
function isAgentModule(mod) {
|
|
18
|
+
return (typeof mod === 'object' &&
|
|
19
|
+
mod !== null &&
|
|
20
|
+
'config' in mod &&
|
|
21
|
+
'createModelCall' in mod &&
|
|
22
|
+
typeof mod.createModelCall === 'function');
|
|
23
|
+
}
|
|
24
|
+
/** Scans `dir`'s direct files only (not subdirectories — put shared,
|
|
25
|
+
* non-agent code in a subdirectory to keep it out of discovery, no
|
|
26
|
+
* separate exclude-list mechanism needed) for `.ts`/`.js` modules, each
|
|
27
|
+
* expected to export `config` and `createModelCall` — the same shape
|
|
28
|
+
* every agents/*.ts file in this repo's own reference app already
|
|
29
|
+
* exports. Throws, rather than skipping, on a file that doesn't match
|
|
30
|
+
* that shape or a duplicate AgentConfig.name — both are almost always a
|
|
31
|
+
* bug in the agent file, not something to silently ignore. */
|
|
32
|
+
export async function discoverAgents(dir) {
|
|
33
|
+
const entries = new Map();
|
|
34
|
+
const files = readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile() && /\.(ts|js)$/.test(entry.name) && !entry.name.endsWith('.d.ts'));
|
|
35
|
+
for (const file of files) {
|
|
36
|
+
const modulePath = resolve(dir, file.name);
|
|
37
|
+
const mod = await import(pathToFileURL(modulePath).href);
|
|
38
|
+
if (!isAgentModule(mod)) {
|
|
39
|
+
throw new Error(`${file.name} in ${dir} does not export both 'config' and 'createModelCall' — every agent module must.`);
|
|
40
|
+
}
|
|
41
|
+
const name = mod.config.name;
|
|
42
|
+
if (entries.has(name)) {
|
|
43
|
+
throw new Error(`Duplicate agent name '${name}' — ${file.name} in ${dir} isn't the first file to declare AgentConfig.name '${name}'.`);
|
|
44
|
+
}
|
|
45
|
+
entries.set(name, mod);
|
|
46
|
+
}
|
|
47
|
+
return entries;
|
|
48
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,3 +9,7 @@ export { createAnthropicModelCall } from './anthropic-model-call.js';
|
|
|
9
9
|
export type { AnthropicModelCallOptions } from './anthropic-model-call.js';
|
|
10
10
|
export { createOpenAIModelCall } from './openai-model-call.js';
|
|
11
11
|
export type { OpenAIModelCallOptions } from './openai-model-call.js';
|
|
12
|
+
export { createDeepSeekModelCall } from './deepseek-model-call.js';
|
|
13
|
+
export type { DeepSeekModelCallOptions } from './deepseek-model-call.js';
|
|
14
|
+
export { discoverAgents } from './discover-agents.js';
|
|
15
|
+
export type { AgentModule } from './discover-agents.js';
|
package/dist/index.js
CHANGED
|
@@ -8,3 +8,5 @@ export { FileSessionStore, RedisSessionStore, createSessionStore } from './sessi
|
|
|
8
8
|
export { VectorIndex, embed, cosineSimilarity } from './vector-index.js';
|
|
9
9
|
export { createAnthropicModelCall } from './anthropic-model-call.js';
|
|
10
10
|
export { createOpenAIModelCall } from './openai-model-call.js';
|
|
11
|
+
export { createDeepSeekModelCall } from './deepseek-model-call.js';
|
|
12
|
+
export { discoverAgents } from './discover-agents.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
|
-
import type { ModelCall } from './run-agent.js';
|
|
2
|
+
import type { Message, ModelCall, ModelResponse } from './run-agent.js';
|
|
3
|
+
type ChatMessage = OpenAI.Chat.Completions.ChatCompletionMessageParam;
|
|
3
4
|
export interface OpenAIModelCallOptions {
|
|
4
5
|
/** Defaults to the OPENAI_API_KEY env var, same as the SDK itself. */
|
|
5
6
|
apiKey?: string;
|
|
@@ -11,4 +12,8 @@ export interface OpenAIModelCallOptions {
|
|
|
11
12
|
/** Inject a pre-configured client instead of building one from apiKey — e.g. to pass a custom `fetch` in tests. */
|
|
12
13
|
client?: OpenAI;
|
|
13
14
|
}
|
|
15
|
+
export declare function toMessageParams(message: Message): ChatMessage[];
|
|
16
|
+
export declare function toStopReason(finishReason: string): string;
|
|
17
|
+
export declare function toModelResponse(choice: OpenAI.Chat.Completions.ChatCompletion.Choice): ModelResponse;
|
|
14
18
|
export declare function createOpenAIModelCall(options: OpenAIModelCallOptions): ModelCall;
|
|
19
|
+
export {};
|
|
@@ -17,8 +17,15 @@
|
|
|
17
17
|
// defaultIsTruncated checks a translated ModelResponse for stop_reason ===
|
|
18
18
|
// 'max_tokens', so a provider-agnostic caller wiring up onTruncated later
|
|
19
19
|
// gets the same behavior regardless of which ModelCall is plugged in.
|
|
20
|
+
//
|
|
21
|
+
// toMessageParams/toStopReason/toModelResponse are exported for
|
|
22
|
+
// deepseek-model-call.ts to reuse verbatim — DeepSeek's Chat Completions
|
|
23
|
+
// API is wire-compatible with this same request/response shape (confirmed
|
|
24
|
+
// against DeepSeek's own docs), the one real difference being the token-
|
|
25
|
+
// limit field name, which is only decided at the request-building call
|
|
26
|
+
// site below, not in these shared translation functions.
|
|
20
27
|
import OpenAI from 'openai';
|
|
21
|
-
function toMessageParams(message) {
|
|
28
|
+
export function toMessageParams(message) {
|
|
22
29
|
if (typeof message.content === 'string') {
|
|
23
30
|
return [{ role: message.role === 'assistant' ? 'assistant' : 'user', content: message.content }];
|
|
24
31
|
}
|
|
@@ -54,7 +61,7 @@ function toMessageParams(message) {
|
|
|
54
61
|
content: block.content ?? '',
|
|
55
62
|
}));
|
|
56
63
|
}
|
|
57
|
-
function toStopReason(finishReason) {
|
|
64
|
+
export function toStopReason(finishReason) {
|
|
58
65
|
if (finishReason === 'tool_calls' || finishReason === 'function_call')
|
|
59
66
|
return 'tool_use';
|
|
60
67
|
if (finishReason === 'length')
|
|
@@ -63,6 +70,26 @@ function toStopReason(finishReason) {
|
|
|
63
70
|
return 'end_turn';
|
|
64
71
|
return finishReason;
|
|
65
72
|
}
|
|
73
|
+
export function toModelResponse(choice) {
|
|
74
|
+
const content = [];
|
|
75
|
+
if (choice.message.content)
|
|
76
|
+
content.push({ type: 'text', text: choice.message.content });
|
|
77
|
+
for (const call of choice.message.tool_calls ?? []) {
|
|
78
|
+
// A 'custom' tool call is the freeform-text tool type OpenAI added
|
|
79
|
+
// alongside 'function' — this framework's ToolDefinition only ever
|
|
80
|
+
// declares 'function'-shaped tools, so a custom call can't have come
|
|
81
|
+
// from anything this loop offered the model.
|
|
82
|
+
if (call.type !== 'function')
|
|
83
|
+
continue;
|
|
84
|
+
content.push({
|
|
85
|
+
type: 'tool_use',
|
|
86
|
+
id: call.id,
|
|
87
|
+
name: call.function.name,
|
|
88
|
+
input: JSON.parse(call.function.arguments),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return { stop_reason: toStopReason(choice.finish_reason), content };
|
|
92
|
+
}
|
|
66
93
|
export function createOpenAIModelCall(options) {
|
|
67
94
|
const client = options.client ?? new OpenAI({ apiKey: options.apiKey });
|
|
68
95
|
const maxTokens = options.maxTokens ?? 4096;
|
|
@@ -78,24 +105,6 @@ export function createOpenAIModelCall(options) {
|
|
|
78
105
|
}))
|
|
79
106
|
: undefined,
|
|
80
107
|
});
|
|
81
|
-
|
|
82
|
-
const content = [];
|
|
83
|
-
if (choice.message.content)
|
|
84
|
-
content.push({ type: 'text', text: choice.message.content });
|
|
85
|
-
for (const call of choice.message.tool_calls ?? []) {
|
|
86
|
-
// A 'custom' tool call is the freeform-text tool type OpenAI added
|
|
87
|
-
// alongside 'function' — this framework's ToolDefinition only ever
|
|
88
|
-
// declares 'function'-shaped tools, so a custom call can't have come
|
|
89
|
-
// from anything this loop offered the model.
|
|
90
|
-
if (call.type !== 'function')
|
|
91
|
-
continue;
|
|
92
|
-
content.push({
|
|
93
|
-
type: 'tool_use',
|
|
94
|
-
id: call.id,
|
|
95
|
-
name: call.function.name,
|
|
96
|
-
input: JSON.parse(call.function.arguments),
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
return { stop_reason: toStopReason(choice.finish_reason), content };
|
|
108
|
+
return toModelResponse(response.choices[0]);
|
|
100
109
|
};
|
|
101
110
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loopengine",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A minimal framework for defining and running tool-using AI agents over CLI, HTTP, or both.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
"dist/anthropic-model-call.d.ts",
|
|
26
26
|
"dist/openai-model-call.js",
|
|
27
27
|
"dist/openai-model-call.d.ts",
|
|
28
|
+
"dist/deepseek-model-call.js",
|
|
29
|
+
"dist/deepseek-model-call.d.ts",
|
|
30
|
+
"dist/discover-agents.js",
|
|
31
|
+
"dist/discover-agents.d.ts",
|
|
28
32
|
"LICENSE"
|
|
29
33
|
],
|
|
30
34
|
"scripts": {
|