@jskit-ai/agent 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,318 @@
1
- # Temporary Holding Version
1
+ # @jskit-ai/agent
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A working agent loop on the AI SDK: call the model, run the tools it asks for, feed the results
4
+ back, until it answers. Around that loop sit the choices that make it finish real jobs: reasoning
5
+ on every step, retries, room inside a long turn, checks on every reply, and summaries sized for a
6
+ 1-million-token window. It has no tables; the opt-in `@jskit-ai/agent-runtime` adds saved runs,
7
+ goals, background summaries and coding tools.
8
+
9
+ The bar it was built to: *"our agentic loop must be NOTHING short of Claude's own"*, for agents that
10
+ create software. This document says what the loop does, every decision behind it and why, how it
11
+ was measured and how good it is. Keep it current when the loop changes. `docs/deepseek-harness.md`
12
+ is the study of DeepSeek Harness (about 165 design choices) the loop was designed from.
13
+
14
+ ```js
15
+ import { chatModel, createTool, runLoop } from "@jskit-ai/agent/server";
16
+ import { z } from "zod";
17
+
18
+ const model = chatModel({ apiKey, model: "deepseek-ai/DeepSeek-V4.1-Flash", baseURL });
19
+ const tools = {
20
+ lookUp: createTool({
21
+ id: "look_up",
22
+ description: "Look a word up in the dictionary.",
23
+ inputSchema: z.object({ word: z.string() }),
24
+ execute: ({ word }) => dictionary.find(word)
25
+ })
26
+ };
27
+ const { text, transcript, unfinished } = await runLoop({
28
+ model, name: "Helper", instructions, history, tools, maxSteps: 40
29
+ });
30
+ ```
31
+
32
+ ---
33
+
34
+ ## 1. Status in one page
35
+
36
+ | | |
37
+ |---|---|
38
+ | **Results** (Together AI, everything in) | shared coding 45/45, hard coding 15/15, app building 15/15 (in a group too), recall after a ~600k-token chat 5/5, quick chat 10/10 in 1–2 s |
39
+ | **Against the bar** | Claude Code running the same DeepSeek model: 15/15 shared, 5/5 hard. The loop matches it on every task measured. DeepSeek's own harness: 44/45 shared; the loop passes it. |
40
+ | **Before this work** | shared 40/45, failure-prone cases 64/70, every group request for an app failed |
41
+ | **Speed** | quick chat 0–1 s; coding tasks 2–25 s; the loop's own overhead per reply 0.05–0.2 s, the rest is the model |
42
+ | **Cost** | about half a US cent per coding task on Together; 65–95% of input tokens come from the cache |
43
+ | **Open** | a mid-turn summary (deferred until a long build shows the need) |
44
+
45
+ ---
46
+
47
+ ## 2. How a reply runs
48
+
49
+ Every reply (a person's own chat, a room, a group, a request from another agent, a routine, an app
50
+ task) goes through **one loop**: `runLoop`, on the AI SDK's `generateText` (or `streamText` with
51
+ `onText`). The app around it chooses the history, the tools and what to do with the result.
52
+
53
+ ```
54
+ person's message
55
+ └─ the app's reply
56
+ ├─ history: summary + messages since (summarise first if past 550k) §5
57
+ ├─ state note: time, accounts, routines, open to-do list, job notes §6
58
+ ├─ tools for this turn (+ repeat reminder, + mid-reply messages) §7
59
+ ├─ runLoop()
60
+ │ ├─ every step reasons (adaptive) §3
61
+ │ ├─ old tool results give way past 700k inside the turn §5
62
+ │ ├─ each finished step can be saved (onStep, onSteps) §8
63
+ │ └─ out of steps or time → account of where it got to §8
64
+ ├─ checks after the reply (each can send it back once): §9
65
+ │ raw markup · stopped short · claimed files · unsupported claims
66
+ ├─ cleaning: no markup, no bracketed notes §9
67
+ └─ unfinished? → a goal, carried on in rounds by itself §8
68
+ ```
69
+
70
+ ### Where the code is
71
+
72
+ | What | File |
73
+ |---|---|
74
+ | The loop, the system prompt in each format, the stored transcript | `src/server/loop.js` |
75
+ | The chat model in either format, reasoning settings, retries, one-shot calls | `src/server/model.js` |
76
+ | Tools as plain objects, the schema the model is shown | `src/server/tool.js`, `schema.js` |
77
+ | History within budget, the summary note, room inside a long turn, clipped old results | `src/server/context.js` |
78
+ | Reply checks: leaked markup, stopping short, the tool record, unsupported claims | `src/server/checks.js` |
79
+ | Summaries without tables: where to cut, what the summariser reads, folding the previous one | `src/server/summaries.js` |
80
+ | Repeat reminders and messages heard mid-reply | `src/server/turn.js` |
81
+ | Token estimates, reading in parts, same gist, bracketed notes | `src/server/text.js` |
82
+ | Saved runs and the resumer, goals, background summaries, to-do list and notes, coding tools | `@jskit-ai/agent-runtime` |
83
+
84
+ ---
85
+
86
+ ## 3. The model and how it's called
87
+
88
+ - **Model:** DeepSeek V4.1 Flash, 1,048,576-token window (`WINDOW_TOKENS`), through **Together AI**
89
+ by default, in the OpenAI style (`@ai-sdk/deepseek`).
90
+ - **Format:** with `anthropicURL` (DeepSeek's own API), `chatModel` calls the model in the
91
+ **Anthropic format** (`@ai-sdk/anthropic`), exactly as Claude Code calls it: signed reasoning sent
92
+ back on every step, adaptive thinking at high effort, a 32,000-token output cap.
93
+ - **Reasoning:** on for **every step of every loop**, adaptive, so the model spends a lot on a hard
94
+ bug and almost nothing on "hi" (`REASONING.on`). One-shot side calls (rule matching, summaries,
95
+ activity lines, the claims checker) don't reason (`REASONING.off`); a call that builds something
96
+ may.
97
+ - **Retries:** the AI SDK retries a failed call 6 times (`MODEL_RETRIES`), waiting 2, 4, 8… seconds
98
+ (about two minutes in all), longer than DeepSeek Harness's five quick tries.
99
+ - **Output cap:** 32,000 tokens for loop steps (`LOOP_OUTPUT_TOKENS`; the Anthropic format needs
100
+ one, and its default is 4,096).
101
+ - **System prompt:** one message in the OpenAI style, parts joined by a blank line; separate blocks
102
+ in the Anthropic format. Instructions, then the summary, then the state note.
103
+
104
+ ## 4. What the agent is told
105
+
106
+ The instructions are the app's, but what was learned writing them belongs with the loop:
107
+
108
+ - **No narration.** Instructions that said "keep the person informed" and asked for status lines
109
+ made DeepSeek announce a step and **end its turn**. Tell the model that the person sees what it's
110
+ doing while it works, so it should call the tools straight away and keep going until the job is
111
+ done. Status lines people rely on can stay, as tool fields.
112
+ - **A to-do list** for any job of more than three steps; **job notes** in long jobs.
113
+ - **Files:** send existing files with a tool, never claim an attachment, never write internal
114
+ paths; name files by content; scratch in `/tmp`.
115
+
116
+ ## 5. Context: using the 1M window without forgetting
117
+
118
+ | Where | What happens | Numbers |
119
+ |---|---|---|
120
+ | Between turns | the conversation is summarised in the background, recall first, keeping the newest part word for word (`SUMMARY_LIMITS`; the store is agent-runtime's) | summarise past **450k** tokens, keep **125k** (up to 200 turns), summary up to 8,000 tokens |
121
+ | Before a reply | if summarising fell behind, the reply **summarises first** instead of cutting (`compactMessages`) | past **550k** |
122
+ | Last resort | if summarising fails, the oldest messages give way, **and the model is told** (`withinBudget`) | past **600k** |
123
+ | Inside one turn | the oldest tool results become a one-line note ("run the tool again if you need it"), never the newest 20; once left out they stay out, so the cache keeps hitting (`trimOldResults`) | past **700k**, down to **500k** |
124
+ | Old results in history | a long tool result from an earlier turn is clipped (`clipOldResult`) | 6,000 characters |
125
+ | Each tool result | command output shows its first and last 8,000 characters; the **whole output is a file**; files are read in parts (`textPart`) | parts of 120,000 characters; whole outputs up to 16 MB |
126
+ | The prompt's start | instructions, then summary, then history; the state note just before the last message | 65–95% of input cached |
127
+
128
+ ## 6. Keeping things in mind
129
+
130
+ - **To-do list** (`update_todos` in agent-runtime): one card in the chat, updated in place; the
131
+ open list is in the state note at every turn. Agents chose to keep one in 10 of 14 hard runs.
132
+ - **Job notes** (`keep_notes`): the agent's private notes for the job, in the state note at every
133
+ turn, never shown in the chat.
134
+ - **Goals** (`create_goal`, `get_goal`, `update_goal`): a multi-step job keeps going in rounds until
135
+ it's complete, blocked (not before round 3), at 24 rounds, or at its cost limit.
136
+
137
+ ## 7. Talking to it while it works
138
+
139
+ What the person writes during a reply reaches the model **with its next tool result**
140
+ (`hearWhileWorking`), so it changes course at once, and that reply then counts as the answer. The
141
+ **3rd, 5th and 8th identical tool call** in a row gets a reminder in its result (`remindOnRepeat`).
142
+
143
+ ## 8. Long jobs and staying alive
144
+
145
+ - **Steps saved as they happen:** `onStep` hears each finished step, so the app can save it
146
+ (agent-runtime's `createRuns`). If the server stops, a page retry or the **resumer** on any server
147
+ carries the reply on from its last step (as Claude Code resumes from its transcript), unless the
148
+ person has written since.
149
+ - **Never "ask me to carry on":** a reply that runs out of steps or time (15 minutes,
150
+ `AGENT_REPLY_MINUTES`) says where it got to and returns `unfinished`; the app opens a goal for the
151
+ request and the rounds carry it on in the background. With `carriesOn`, the account says the work
152
+ continues.
153
+ - **Limits, said plainly in the chat:** 24 rounds, and a **cost limit per job**,
154
+ `AGENT_JOB_COST_USD` (default 5 dollars, 0 for none).
155
+ - **Stop:** aborting `signal` ends the run at once with the signal's reason.
156
+ - **Callbacks checked on entry:** each callback option (`onSteps`, `onStep`, `throwIf`,
157
+ `onCompacting`, and agent-runtime's goal and summary hooks) is a function, or left out for its
158
+ named default. Anything else, `null` included, fails at once with a TypeError naming the option,
159
+ instead of inside a step callback, where the AI SDK drops the error.
160
+
161
+ ## 9. Truthful replies
162
+
163
+ Each check below can send a reply back **once**, carrying on from its own steps
164
+ (`withoutClosingWords`: the second pass sees the first one's work, so nothing is redone).
165
+
166
+ | Check | Catches |
167
+ |---|---|
168
+ | **Raw markup** (`LEAKED_MARKUP`) | a tool call written as text, DeepSeek's tokens (`<|begin▁of▁sentence|>`), budget and safety tags |
169
+ | **Stopped short** (`stoppedShort`) | a reply under 600 characters that ends by announcing its next step; any agent with tools, groups too (with a goal active, its round carries on instead). A reply that ends with the loop's own account (`account` in its result) is left alone: the account says where it got to |
170
+ | **Claimed files** | "attached below" with no file sent (the app's check, by words) |
171
+ | **Unsupported claims** (`unsupportedClaims`) | a reply that reports actions is compared with what its tools actually did (`toolRecord`), in one short call; "emailed it to Sam" with no email is sent back |
172
+
173
+ And always: **no raw markup** (`withoutMarkup`) and **no bracketed bookkeeping notes**
174
+ (`stripNotes`) in what people see.
175
+
176
+ An app can add its own: a builder agent that saves a draft only when the draft passes the app's own
177
+ checks, and only scripts whose tests pass.
178
+
179
+ ---
180
+
181
+ ## 10. Decisions, and why
182
+
183
+ | # | Decision | Why |
184
+ |---|---|---|
185
+ | 1 | Build our own loop (first on Mastra, on the AI SDK since), studying DeepSeek Harness rather than depending on it | DSH is a preview with ~80 packages; its designs are MIT and documented in `docs/deepseek-harness.md`. The move off Mastra kept every request byte for byte |
186
+ | 2 | Measure first: fixed tasks judged by outcome, baseline before changes | so every change is proven by numbers |
187
+ | 3 | Claude Code's own loop is the bar | "NOTHING short of Claude's own" |
188
+ | 4 | Drop narration instructions | measured cause of "announce and stop" (60/60 without, 53/60 with) |
189
+ | 5 | Reasoning on every step, adaptive | reasoning on; adaptive keeps chat at 0–1 s |
190
+ | 6 | DeepSeek's own API in Claude Code's format, as an option | 70/70 and cached; Together stays the default |
191
+ | 7 | Together stays the provider | with the loop changes it scores 70/70 there too |
192
+ | 8 | Size everything for 1M | "keep in mind that we have ONE MILLION context" |
193
+ | 9 | Never forget unseen | summarise first; say so if anything is cut |
194
+ | 10 | Trim old tool results inside a turn rather than summarise mid-turn | the same "leave out, re-read when needed" idea as Claude Code; the gist stays in the model's words, notes and to-do list. A mid-turn summary waits for a real need |
195
+ | 11 | Whole command outputs as files | as Claude Code keeps large outputs: the agent can grep and tail them; on storage every server shares |
196
+ | 12 | Save steps as they happen; resume on any server | Claude Code survives being killed; servers are disposable |
197
+ | 13 | Messages mid-reply reach the model at its next step | conversations *while* it's doing things |
198
+ | 14 | To-do list as a live card; job notes private | the person sees the plan; notes complete it |
199
+ | 15 | No subagents yet | with 1M tokens the main reason (a small window) mostly goes; they bring cost and risk |
200
+ | 16 | A cost limit per job, as a setting | a limit, but a parameter |
201
+ | 17 | Truthful replies, fully built | replies that claim work not done were the worst failures |
202
+ | 18 | A long job carries on by itself | the person is never asked to say "carry on" |
203
+ | 19 | One loop for groups too | groups gained the deadline, the account and trimming |
204
+
205
+ What didn't work, or wasn't needed:
206
+ - **Not sending reasoning back** on Together: helped (64 to 68 of 70) but wasn't the answer; the
207
+ narration and stop-short fixes were.
208
+ - **Together's Anthropic endpoint:** 70/70, but no prompt cache and unsigned reasoning.
209
+ - **Removing every tool's acknowledgement field:** it broke status lines people relied on, so only
210
+ the instruction changed.
211
+ - **Our own retry code:** removed; the libraries already retry.
212
+
213
+ ---
214
+
215
+ ## 11. How it was measured
216
+
217
+ The loop was measured inside the first app built on it, with its own runners: a real reply through
218
+ the whole pipeline (every tool, the state note, the checks), against a test database, with the real
219
+ model; and the same shared cases through DeepSeek Harness, for reference. Judges are hidden from the
220
+ agent: they're copied in only after it finishes. Folder cases judge the files; app cases judge the
221
+ apps, tables and records; chat cases judge the words. Every hard case was checked against a
222
+ reference solution (passes) and against the shipped files (fails). Variants compared: as shipped,
223
+ quiet, minimal tools, no reasoning sent back, Together's Anthropic endpoint, DeepSeek's endpoint as
224
+ Claude Code calls it, DeepSeek in the OpenAI style, and thinking off.
225
+
226
+ ### The cases
227
+
228
+ **Shared coding (15):** each gets a folder; the judge checks the result.
229
+
230
+ | Case | The job | Judged on |
231
+ |---|---|---|
232
+ | big-file-edit | change one helper in a 1,200-line module, nothing else | the helper's new behaviour, every other line untouched |
233
+ | bug-fix | fix two bugs in `money.py` so its tests pass | hidden tests of the documented behaviour |
234
+ | contacts-extract | pull emails and Australian phone numbers from messy text | exact normalised, sorted lists; no versions or prices |
235
+ | crash-debug | make a crashing stats script skip bad values | both commands' exact output |
236
+ | csv-summary | totals per region across three CSVs | `summary.csv` against the expected totals |
237
+ | date-ranges | merge touching and overlapping booking ranges, with tests | hidden tests including invalid input |
238
+ | fix-js-bugs | fix two JavaScript files so `npm test` passes | hidden tests |
239
+ | four-deliverables | a script, its tests, its output and a README | all four exist and are right |
240
+ | json-flatten | flatten nested orders into CSV lines | exact rows and totals |
241
+ | log-report | top IPs and 5xx counts from a 60,000-line log | `report.json` against the expected answer |
242
+ | notes-needle | three facts hidden in 40 meeting notes | the facts and the files they're in |
243
+ | refactor-js | rename a function everywhere and add a discount parameter, with tests | hidden tests; the old name gone |
244
+ | sqlite-questions | four questions about a SQLite shop database | exact answers |
245
+ | textstats | word counts and top words, with own tests | hidden tests (apostrophes, ties) |
246
+ | todo-cli | a planned command-line to-do app with subprocess tests | hidden CLI tests (ids never reused, errors) |
247
+
248
+ **Hard coding (5):** longer, trickier jobs where loops fall over.
249
+
250
+ | Case | The job | Judged on |
251
+ |---|---|---|
252
+ | calc | build a calculator language (precedence, power, variables, functions, exact error positions) from a 17-test spec, no eval | the spec plus hidden programs and errors |
253
+ | billing | "March invoices are wrong": three bugs across modules (CSV thousands, month end, tiers) that only show when run | every invoice regenerated and exact |
254
+ | notes-api | a JSON HTTP API with validation, filters and persistence, plus tests | the server started and probed over HTTP, including a restart |
255
+ | async-migration | convert four callback modules and their tests to async/await, a test per error case | hidden async tests, no callbacks left, `npm test` passes |
256
+ | dedupe-perf | make a quadratic dedupe fast (under 10 s on 60,000 rows) without changing output for any input | output identical to the original on 30 tricky random inputs, the sample, and the big file in time |
257
+
258
+ **App building (5):** a throwaway workspace, the agent as its builder: a new app with a table and a
259
+ form, a new field shown on a list, a tested rule that refuses bad records, records added by name,
260
+ and the first of these asked in a group chat. Judged on the tables, apps, scripts and records, and
261
+ on the app's own checks passing.
262
+
263
+ **Quick chat (5):** a greeting, 15% of 84, three bakery names, a friendlier rewrite, a 20-minute
264
+ dinner idea. Judged on a sensible answer; timed.
265
+
266
+ **Memory (1):** a 2.4M-character chat (about 600k tokens) with five facts in its older part (a
267
+ birthday, a café name, a plumber's name and visit, a budget, an allergy), asked for all five. Judged
268
+ on a written summary plus all five facts; facts the agent saved elsewhere are cleared so they can't
269
+ answer for the summary.
270
+
271
+ ### Results over time (Together unless noted)
272
+
273
+ | Run | Shared | Failure-prone (7 × 10) | Hard | Apps |
274
+ |---|---|---|---|---|
275
+ | DeepSeek Harness, reference | 44/45 | — | — | — |
276
+ | Baseline, before changes | 40/45 | — | — | 7/12 |
277
+ | Reasoning from the first step | 43/45 | 53/60 (6 cases) | — | 11/12 |
278
+ | No narration | 44/45 | 60/60 (6 cases), 64/70 | — | 12/12 |
279
+ | Format test: DeepSeek's endpoint, Claude Code's way | — | 70/70 | 9/10 | 12/12 |
280
+ | Whole loop on Together | 45/45 | 70/70 | 14/15, then 15/15 | 15/15 |
281
+ | With the final-reply check | 30/30 | — | — | 10/10 |
282
+ | With commands in gVisor | 30/30 | — | 5/5 | 5/5 |
283
+
284
+ **References on the same tasks:** Claude Code with Sonnet 15/15 shared; Claude Code with DeepSeek
285
+ (the same model) 15/15 shared and 5/5 hard, 97.8% of its input cached.
286
+
287
+ ---
288
+
289
+ ## 12. How good is it
290
+
291
+ **Strong:**
292
+ - On every task measured, it matches Claude Code running the same model, and passes DeepSeek's own
293
+ harness.
294
+ - The failures that mattered are gone: announcing a step and stopping, raw markup, claiming work
295
+ that wasn't done, losing a reply when a server stops, and forgetting old conversation.
296
+ - It's built for apps with several disposable servers, beyond what Claude Code needs: any server
297
+ can resume any reply, jobs carry on unattended in rounds, and everything is in PostgreSQL or on
298
+ shared storage.
299
+
300
+ **Not yet proven:**
301
+ - **Very long builds.** The tasks run from seconds to a few minutes. A real multi-hour build, where
302
+ in-turn trimming and summaries matter most, hasn't been measured; that's where a mid-turn summary
303
+ might turn out to be needed.
304
+ - **Breadth.** About 30 tasks. They're varied and judged strictly, but small next to real use.
305
+ - **The final-reply check's hit rate.** Its prompts aren't stored, so it's known to cause no
306
+ failures but not how often it fires.
307
+
308
+ **Costs:** the final-reply check adds about 1–2 s to replies that report actions; everything else
309
+ around the model costs about 0.05–0.2 s a reply.
310
+
311
+ ## 13. Keeping it good
312
+
313
+ - Rerun the suites after any change to the loop, instructions or tools: shared, hard and app
314
+ building three times each, chat and memory once. No case may get worse without an explanation.
315
+ - Add a case for every failure seen in real use, with a hidden judge checked against a reference
316
+ solution.
317
+ - Every model-facing string, prompt, reasoning setting and tool definition is part of the measured
318
+ behaviour: change one only with a rerun. The package tests (`npm test`) cover the pure parts.