loopengine 0.0.3 → 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 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), and `createAnthropicModelCall`/
78
- `createOpenAIModelCall` (real, ready-to-use `ModelCall`s). See `index.ts` for
79
- the exact list.
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 — build your
83
- own, same one-line-per-agent shape), `adapters/cli.ts`/`adapters/http.ts`
84
- (one specific CLI/HTTP wiring choice, not imposed on every consumer), and
85
- the `Dockerfile`. Everything below this point describes that reference app —
86
- read it as a worked example of what to build with the library, not as
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
- To make a new agent runnable through the adapters, add one line to
134
- `agent-registry.ts` mapping its name to its `config` and a `createModelCall`
135
- factory.
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
- agent-registry.ts Maps agent name -> {config, createModelCall}
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
@@ -26,6 +26,14 @@ export interface AgentConfig {
26
26
  skillsDirs?: string[];
27
27
  skillIndexBudgetTokens?: number;
28
28
  contextBudgetTokens?: number;
29
+ /** Hard cap on model calls in one runAgent() invocation — the only thing
30
+ * that stops a model stuck re-requesting the same (or ping-ponging)
31
+ * tool calls forever, which nothing else in this loop bounds. Default
32
+ * 25. Hitting it ends the turn the same way running out of tools to
33
+ * call does — a real result, not a thrown error — with
34
+ * RunAgentResult.stopReason set to 'max_turns' so a caller can tell the
35
+ * difference from a normal finish. */
36
+ maxTurns?: number;
29
37
  /** Which tools ToolLane may run in a parallel lane. Default: none (every tool runs solo). */
30
38
  isSafeTool?: SafetyClassifier;
31
39
  /** How adapters/http.ts derives a session key from a request body — this
@@ -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
- const choice = response.choices[0];
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
  }
@@ -53,5 +53,10 @@ export interface RunAgentResult {
53
53
  * only works if `history` only ever grows, which stops being true the
54
54
  * moment recovery can shrink/rewrite it, not just extend it. */
55
55
  newMessages: Message[];
56
+ /** Set to 'max_turns' if the loop stopped because it hit
57
+ * config.maxTurns, not because the model produced a final answer with
58
+ * no more tool_use blocks — `text` in that case is a synthetic notice,
59
+ * not something the model said. Absent on a normal finish. */
60
+ stopReason?: 'max_turns';
56
61
  }
57
62
  export declare function runAgent(config: AgentConfig, modelCall: ModelCall, userMessage: string, history?: Message[], options?: RunAgentOptions): Promise<RunAgentResult>;
package/dist/run-agent.js CHANGED
@@ -128,7 +128,19 @@ export async function runAgent(config, modelCall, userMessage, history = [], opt
128
128
  const systemPrompt = skillIndex.length
129
129
  ? `${config.systemPrompt}\n\nAvailable skills:\n${skillIndex.map((s) => `- ${s.name}: ${s.description}`).join('\n')}`
130
130
  : config.systemPrompt;
131
+ // Nothing else in this loop bounds a model stuck re-requesting the same
132
+ // (or ping-ponging) tool calls forever — checked before spending a model
133
+ // call on the (maxTurns + 1)-th turn, not after.
134
+ const maxTurns = config.maxTurns ?? 25;
135
+ let turn = 0;
131
136
  for (;;) {
137
+ turn++;
138
+ if (turn > maxTurns) {
139
+ const text = `Stopped after ${maxTurns} turns without a final answer — the agent may be stuck in a loop.`;
140
+ pushMessage({ role: 'assistant', content: text });
141
+ log('loop:max_turns', { maxTurns });
142
+ return { text, history: messages, newMessages, stopReason: 'max_turns' };
143
+ }
132
144
  const budget = contextClip.check(messages.map(flattenForContextClip));
133
145
  log('contextclip:check', budget);
134
146
  if (budget.action === 'nudge' && budget.nudge)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopengine",
3
- "version": "0.0.3",
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": {