loopengine 0.0.4 → 0.0.7
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 +63 -14
- 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 +6 -1
package/README.md
CHANGED
|
@@ -32,6 +32,28 @@ one layer further out still: it's not part of the loop either, just a
|
|
|
32
32
|
its output merges straight in with no adapter code. See "External tool
|
|
33
33
|
gateways" below.
|
|
34
34
|
|
|
35
|
+
## Creating a new project
|
|
36
|
+
|
|
37
|
+
The fastest way to get a running agent server, in your own repo, is
|
|
38
|
+
[`create-loopengine`](https://www.npmjs.com/package/create-loopengine):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx create-loopengine@latest my-agents
|
|
42
|
+
cd my-agents
|
|
43
|
+
npm install
|
|
44
|
+
cp .env.example .env # fill in ANTHROPIC_API_KEY
|
|
45
|
+
npm run dev # HTTP server on :8787
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
That scaffolds a real, standalone project — its own `package.json`
|
|
49
|
+
depending on `loopengine` (not a clone of this repo), an HTTP + CLI
|
|
50
|
+
adapter, and one starter agent — rather than a bare library import. `npm
|
|
51
|
+
install loopengine` alone (below) only gets you the library; the server,
|
|
52
|
+
adapters, and a runnable example are deliberately not part of that
|
|
53
|
+
package, which is the gap `create-loopengine` fills. See
|
|
54
|
+
[`create-loopengine`'s own README](https://github.com/evanyan22/create-loopengine)
|
|
55
|
+
for what the generated project looks like.
|
|
56
|
+
|
|
35
57
|
## Using loopengine as a library
|
|
36
58
|
|
|
37
59
|
This repo is two things in one: a small library (the loop itself) and a
|
|
@@ -74,17 +96,20 @@ console.log(result.text)
|
|
|
74
96
|
|
|
75
97
|
The full exported surface: `runAgent`, `AgentConfig`/`ToolSchema`/`ToolDefinition`,
|
|
76
98
|
`FileSessionStore`/`RedisSessionStore`/`createSessionStore`, `VectorIndex`/
|
|
77
|
-
`embed`/`cosineSimilarity` (for RAG — see below),
|
|
78
|
-
`
|
|
79
|
-
|
|
99
|
+
`embed`/`cosineSimilarity` (for RAG — see below), `discoverAgents` (scans a
|
|
100
|
+
directory for `config`/`createModelCall` modules and builds a name → entry
|
|
101
|
+
map, keyed by `AgentConfig.name` — see `agent-registry.ts` for the ~15-line
|
|
102
|
+
wrapper this repo's own adapters use), and `createAnthropicModelCall`/
|
|
103
|
+
`createOpenAIModelCall`/`createDeepSeekModelCall` (real, ready-to-use
|
|
104
|
+
`ModelCall`s). See `index.ts` for the exact list.
|
|
80
105
|
|
|
81
106
|
**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.
|
|
107
|
+
`agent-registry.ts` (this repo's own name → config lookup table, built on
|
|
108
|
+
`discoverAgents` — build your own the same way), `adapters/cli.ts`/
|
|
109
|
+
`adapters/http.ts` (one specific CLI/HTTP wiring choice, not imposed on
|
|
110
|
+
every consumer), and the `Dockerfile`. Everything below this point
|
|
111
|
+
describes that reference app — read it as a worked example of what to
|
|
112
|
+
build with the library, not as library API itself.
|
|
88
113
|
|
|
89
114
|
## Quick start
|
|
90
115
|
|
|
@@ -130,9 +155,10 @@ export const config: AgentConfig = {
|
|
|
130
155
|
`agents/file-agent.ts` and `agents/customer-service-agent.ts` are two complete, working
|
|
131
156
|
examples — same `runAgent` loop, entirely different personas and tools.
|
|
132
157
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
158
|
+
A file exporting `config` and `createModelCall` and saved into `agents/` is
|
|
159
|
+
runnable through both adapters immediately — `agent-registry.ts` finds it
|
|
160
|
+
automatically (by `AgentConfig.name`, not the filename) at startup.
|
|
161
|
+
Nothing to edit, no import to add, no adapter change.
|
|
136
162
|
|
|
137
163
|
## Retrieval (RAG)
|
|
138
164
|
|
|
@@ -235,7 +261,7 @@ conversation on a later request):
|
|
|
235
261
|
```bash
|
|
236
262
|
curl -X POST localhost:8787/agents/file-agent/messages \
|
|
237
263
|
-H 'content-type: application/json' \
|
|
238
|
-
-d '{"
|
|
264
|
+
-d '{"message":"Summarize a.txt and b.txt into summary.txt."}'
|
|
239
265
|
```
|
|
240
266
|
|
|
241
267
|
`customer-service` is the exception here too — a request with no
|
|
@@ -385,6 +411,18 @@ const modelCall = createOpenAIModelCall({ model: 'gpt-4.1' })
|
|
|
385
411
|
const result = await runAgent(config, modelCall, 'order A-1001 arrived broken', [])
|
|
386
412
|
```
|
|
387
413
|
|
|
414
|
+
```ts
|
|
415
|
+
import { createDeepSeekModelCall } from './deepseek-model-call.js'
|
|
416
|
+
|
|
417
|
+
// model is required, same reasoning as OpenAI's; reads DEEPSEEK_API_KEY
|
|
418
|
+
// from the env — throws immediately, with a DeepSeek-specific message, if
|
|
419
|
+
// neither that nor apiKey is set, rather than the OpenAI SDK's own
|
|
420
|
+
// constructor silently accepting OPENAI_API_KEY instead.
|
|
421
|
+
const modelCall = createDeepSeekModelCall({ model: 'deepseek-chat' })
|
|
422
|
+
|
|
423
|
+
const result = await runAgent(config, modelCall, 'order A-1001 arrived broken', [])
|
|
424
|
+
```
|
|
425
|
+
|
|
388
426
|
Nothing else in `run-agent.ts`, the adapters, or any `AgentConfig` needs to
|
|
389
427
|
change — `ModelCall` is the only seam a real API call needs.
|
|
390
428
|
|
|
@@ -410,6 +448,15 @@ user-role message (mirroring Anthropic's shape); OpenAI has no equivalent —
|
|
|
410
448
|
each becomes its own top-level `role: 'tool'` message, so one loopengine
|
|
411
449
|
`Message` can expand into several OpenAI messages, never the reverse.
|
|
412
450
|
|
|
451
|
+
`deepseek-model-call.ts` reuses `openai-model-call.ts`'s translation
|
|
452
|
+
verbatim (exported, not duplicated) rather than reimplementing it —
|
|
453
|
+
DeepSeek's Chat Completions API is wire-compatible with OpenAI's (verified
|
|
454
|
+
against DeepSeek's own docs), down to the same `tool_calls` response
|
|
455
|
+
shape. The one real difference: DeepSeek documents `max_tokens`, not the
|
|
456
|
+
newer `max_completion_tokens` OpenAI's endpoint expects, so only the
|
|
457
|
+
request-building call site differs, not the shared message/response
|
|
458
|
+
translation functions.
|
|
459
|
+
|
|
413
460
|
## Skills
|
|
414
461
|
|
|
415
462
|
Agents can discover and invoke `SKILL.md` files (see `skills/`), the same
|
|
@@ -446,10 +493,12 @@ index.ts Public API surface — what actually ships to npm
|
|
|
446
493
|
agent-config.ts AgentConfig type — the thing you fill in to define an agent
|
|
447
494
|
run-agent.ts The generic ReAct loop every agent and adapter runs through
|
|
448
495
|
vector-index.ts Dependency-free embeddings + cosine-similarity search, for RAG
|
|
449
|
-
|
|
496
|
+
discover-agents.ts discoverAgents — scans a directory, builds {name -> {config, createModelCall}}
|
|
497
|
+
agent-registry.ts This repo's ~15-line wrapper: discoverAgents('agents/') + listAgents()/getEntry()
|
|
450
498
|
session-store.ts SessionStore: FileSessionStore, RedisSessionStore, createSessionStore()
|
|
451
499
|
anthropic-model-call.ts createAnthropicModelCall — a real ModelCall this repo ships
|
|
452
500
|
openai-model-call.ts createOpenAIModelCall — same seam, OpenAI's Chat Completions API
|
|
501
|
+
deepseek-model-call.ts createDeepSeekModelCall — same seam, reuses openai-model-call.ts's translation
|
|
453
502
|
agents/file-agent.ts Example agent: summarizes text files
|
|
454
503
|
agents/customer-service-agent.ts Example agent: order/shipment lookup, refund, email
|
|
455
504
|
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,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loopengine",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
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",
|
|
7
|
+
"homepage": "https://loopengine.co",
|
|
7
8
|
"repository": {
|
|
8
9
|
"type": "git",
|
|
9
10
|
"url": "git+https://github.com/evanyan22/loopengine.git"
|
|
@@ -25,6 +26,10 @@
|
|
|
25
26
|
"dist/anthropic-model-call.d.ts",
|
|
26
27
|
"dist/openai-model-call.js",
|
|
27
28
|
"dist/openai-model-call.d.ts",
|
|
29
|
+
"dist/deepseek-model-call.js",
|
|
30
|
+
"dist/deepseek-model-call.d.ts",
|
|
31
|
+
"dist/discover-agents.js",
|
|
32
|
+
"dist/discover-agents.d.ts",
|
|
28
33
|
"LICENSE"
|
|
29
34
|
],
|
|
30
35
|
"scripts": {
|