@tianmucreations/jeeves 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +37 -17
- package/README.md +82 -18
- package/bin/jeeves +8 -1
- package/dist/agent/auto-ids.js +66 -0
- package/dist/agent/auto.js +178 -0
- package/dist/agent/context.js +55 -13
- package/dist/agent/errors.js +83 -22
- package/dist/agent/expert-chat.js +33 -0
- package/dist/agent/housekeeping.js +55 -0
- package/dist/agent/loop.js +174 -12
- package/dist/agent/permissions.js +186 -3
- package/dist/agent/research-gate.js +267 -0
- package/dist/agent/review.js +135 -0
- package/dist/agent/spending.js +73 -0
- package/dist/agent/systemPrompt.js +112 -0
- package/dist/agent/trust.js +29 -0
- package/dist/app.js +31 -11
- package/dist/checkpoints/index.js +103 -0
- package/dist/checkpoints/store.js +239 -0
- package/dist/commands/address.js +5 -0
- package/dist/commands/clear.js +2 -0
- package/dist/commands/help.js +8 -4
- package/dist/commands/keys.js +1 -1
- package/dist/commands/verbose.js +1 -1
- package/dist/components/AddressPrompt.js +31 -0
- package/dist/components/Footer.js +74 -102
- package/dist/components/Input.js +115 -29
- package/dist/components/KeysManager.js +65 -20
- package/dist/components/ModelPicker.js +348 -75
- package/dist/components/ProjectPicker.js +4 -1
- package/dist/components/Transcript.js +29 -14
- package/dist/components/input-layout.js +92 -0
- package/dist/components/transcript-layout.js +27 -19
- package/dist/index.js +25 -7
- package/dist/ink/AlternateScreen.js +33 -16
- package/dist/ink/cursor.js +18 -0
- package/dist/ink/mouse.js +48 -0
- package/dist/keys/store.js +2 -1
- package/dist/models/registry.js +18 -2
- package/dist/platform/config.js +71 -7
- package/dist/providers/catalogue.js +293 -0
- package/dist/providers/direct-services.js +65 -0
- package/dist/providers/direct.js +145 -0
- package/dist/providers/index.js +123 -13
- package/dist/providers/models-snapshot.js +1037 -0
- package/dist/providers/ollama.js +21 -4
- package/dist/providers/openrouter.js +39 -4
- package/dist/providers/step-control.js +28 -0
- package/dist/providers/zai.js +31 -11
- package/dist/state/session.js +110 -36
- package/dist/state/today-spend.js +26 -0
- package/dist/tools/index.js +123 -11
- package/dist/tools/runBash.js +58 -11
- package/dist/tools/web/htmlToText.js +32 -0
- package/dist/tools/web/openrouterChat.js +31 -0
- package/dist/tools/web/research.js +191 -0
- package/package.json +33 -7
package/LICENSE
CHANGED
|
@@ -1,21 +1,41 @@
|
|
|
1
|
-
|
|
1
|
+
Jeeves Licence
|
|
2
|
+
Copyright (c) 2026 Tianmu Creations. All rights reserved.
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
This licence applies to Jeeves version 0.3.1 and every later version. Versions
|
|
5
|
+
0.3.0 and earlier were released under the MIT licence, which still applies to
|
|
6
|
+
those versions.
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
8
|
+
1. YOU MAY
|
|
9
|
+
- Install Jeeves and use it free of charge, for personal use and for work,
|
|
10
|
+
on as many of your own computers as you like.
|
|
11
|
+
- Read the source code on GitHub.
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
2. YOU MAY NOT, without written permission from Tianmu Creations
|
|
14
|
+
- copy Jeeves, or any part of its source code, except as needed to install
|
|
15
|
+
and run it;
|
|
16
|
+
- change, adapt, translate, or build other software from Jeeves or its
|
|
17
|
+
source code;
|
|
18
|
+
- merge Jeeves, or any part of it, into other software;
|
|
19
|
+
- publish, share, distribute, host, or make Jeeves available to others,
|
|
20
|
+
changed or unchanged, except by pointing them to the official copies on
|
|
21
|
+
npm (npmjs.com/package/@tianmucreations/jeeves) and GitHub;
|
|
22
|
+
- sublicense, rent, lease, lend, or sell Jeeves or access to it; or
|
|
23
|
+
- remove or change this licence or any copyright notice.
|
|
14
24
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
3. OTHER PEOPLE'S WORK
|
|
26
|
+
Jeeves uses add-ons made by others, which are installed alongside it and keep
|
|
27
|
+
their own licences. Its built-in list of AI models and prices comes from
|
|
28
|
+
models.dev (MIT licence).
|
|
29
|
+
|
|
30
|
+
4. NO WARRANTY
|
|
31
|
+
Jeeves is provided "as is", without any warranty of any kind. Jeeves asks
|
|
32
|
+
before changing your computer and keeps backups for /undo, but you are
|
|
33
|
+
responsible for what you approve and for the costs of the AI services you
|
|
34
|
+
connect it to. To the fullest extent the law allows, Tianmu Creations is not
|
|
35
|
+
liable for any loss or damage arising from using Jeeves.
|
|
36
|
+
|
|
37
|
+
5. ENDING
|
|
38
|
+
This licence ends automatically if you break it. When it ends you must stop
|
|
39
|
+
using Jeeves and delete your copies.
|
|
40
|
+
|
|
41
|
+
Questions or permission requests: https://tianmucreations.com
|
package/README.md
CHANGED
|
@@ -1,32 +1,96 @@
|
|
|
1
1
|
# Jeeves
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Your personal assistant in the terminal — built for non-coders and coders alike. Say what you need in plain English; Jeeves does the work, carefully, and tells you what it cost.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Jeeves reads and writes your files, tidies folders, builds web pages, fixes programs and checks facts on the web — all from one calm window, in ordinary language. He asks before changing anything, can put things back with `/undo`, and keeps an eye on your costs. No commands to learn, no settings to fiddle with — and if you do code, every model and every detail is there when you want it.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm install -g @tianmucreations/jeeves
|
|
9
|
+
jeeves
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Needs [Node.js](https://nodejs.org) 22.12 or newer, and a key from an AI service — one [OpenRouter](https://openrouter.ai/settings/keys) key is all it takes.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## What you can ask him
|
|
17
|
+
|
|
18
|
+
- "Write a polite letter to my landlord about the broken boiler and save it as letter.txt."
|
|
19
|
+
- "Tidy my Downloads folder: pictures in one place, documents in another, spreadsheets in a third."
|
|
20
|
+
- "Make a one-page website for my bakery with our opening hours and prices."
|
|
21
|
+
- "From hours.csv, make an invoice for my client Brightside, with a total at the bottom."
|
|
22
|
+
- "Our phone number changed — update it everywhere on the website, however it's written."
|
|
23
|
+
- "What's the latest version of Node.js?" — he looks it up and tells you where he found it.
|
|
24
|
+
|
|
25
|
+
## Why people choose Jeeves
|
|
26
|
+
|
|
27
|
+
**Built for people who don't code.** Every screen is a list you move through with the arrow keys and Enter. Open a recent project or create a new one from a list — no typing folder locations, no jargon, no hidden shortcuts. Errors come in plain English ("OpenAI didn't accept the key — type /keys to check or replace it"), never as codes.
|
|
28
|
+
|
|
29
|
+
**A butler, not a chatbot.** Jeeves is polite, unflappable and discreet, in the tradition of P.G. Wodehouse — and he calls you whatever you like (he asks on first launch; `/address` changes it). He answers questions directly, gets on with jobs when asked, and says plainly when something is done.
|
|
30
|
+
|
|
31
|
+
**Auto: the right model, without thinking about it.** Choose Auto and Jeeves works with a fast, inexpensive model, calls in an expert model when a job gets hard, and has the expert double-check finished programs and documents before telling you they're done. The expert must show a concrete example of any problem, and Jeeves must see the problem for himself before changing anything, so good work is never "fixed" into bad work. If the job still proves difficult, he asks before using the strongest (and dearest) model.
|
|
32
|
+
|
|
33
|
+
**He doesn't guess.** Facts about the outside world are looked up on the web, read from the most official page, and given with their source. Before building something new, he checks what already exists and shows you a short research note: use an existing tool, adapt one (licence permitting), or build new — and why. If the same problem happens twice, he researches the cause instead of patching blindly.
|
|
6
34
|
|
|
7
|
-
|
|
35
|
+
**Safe to say yes to.** Anything that changes your computer asks first — or answer **a** once to always allow changes inside that project folder (`/ask` turns asking back on). Before any change in your project folder he quietly takes a backup, so `/undo` puts the folder back — changed and deleted files return, new ones disappear. Changes outside the folder always ask, with a warning, because they can't be undone.
|
|
8
36
|
|
|
9
|
-
|
|
10
|
-
- Windows and Linux will be verified on those machines after the MVP, with any fixes applied to this same codebase - no platform forks.
|
|
37
|
+
**Type while he works.** Your message box grows as you type, and anything you send while Jeeves is busy waits its turn and goes as soon as he finishes. Your own messages sit on a soft grey band, so they're easy to find when you scroll back.
|
|
11
38
|
|
|
12
|
-
|
|
39
|
+
**Honest about costs.** A daily spending limit (you choose it; $3 to start) stops and asks before going over, and a long job checks in every 50 cents. Nothing is spent without you seeing it — and it can cost nothing at all: OpenRouter's free models, or models running on your own computer with Ollama.
|
|
13
40
|
|
|
14
|
-
|
|
15
|
-
2. `npm install`
|
|
16
|
-
3. `npm run dev`
|
|
41
|
+
**Your keys stay private.** Keys are kept in your computer's own secure keychain, never in a plain file.
|
|
17
42
|
|
|
18
|
-
|
|
43
|
+
**See from across the room whether he's finished.** A traffic light in the top right of the window pulses green while Jeeves is working, turns amber when he needs your OK, and shows red when he's done and waiting for you. The same status appears in the Terminal tab's title, so you can see it from another app — and on a Mac a notification tells you when a long job has finished.
|
|
19
44
|
|
|
20
|
-
|
|
45
|
+
**An information bar that only speaks when it matters.** Along the bottom: the model at work (in Auto, which one is working right now), what today has cost against your limit, and — with OpenRouter — what's left on your account. Warnings — credit running low, the limit reached — appear only when something needs you.
|
|
46
|
+
|
|
47
|
+
**Every step visible, never noisy.** Each action is one tidy line that updates in place — "✓ Read letter.txt", "✓ Wrote 1 file" — and research notes are shown in full. The model's thinking stays out of the way unless you ask for it (`Ctrl+R`).
|
|
48
|
+
|
|
49
|
+
## For people who do code
|
|
50
|
+
|
|
51
|
+
- **Any model, your choice.** Turn Auto off and pick from 400+ models through OpenRouter, with search, favourites, prices, memory sizes and a free-models list.
|
|
52
|
+
- **Direct connections** with your own key: Anthropic, OpenAI, Google, xAI (Grok), Mistral and Groq — each company's live model list, with costs worked out from its price list. Plus **any OpenAI-compatible service** (paste its address and key), **Ollama** for models on your own machine, and **Z.ai's GLM Coding Plan** at a flat monthly price.
|
|
53
|
+
- **Prompt caching** where it matters: sticky routing on OpenRouter, and cache markers on Anthropic, so long conversations cost a fraction of the fresh price.
|
|
54
|
+
- **Runs your real tools** — tests, builds, git — and reads the results before claiming a job is done.
|
|
55
|
+
- **Tested on every change** on macOS, Windows and Linux.
|
|
56
|
+
|
|
57
|
+
## Where Auto is available
|
|
58
|
+
|
|
59
|
+
| Service | Auto | Everyday model + expert |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| OpenRouter | Yes | DeepSeek V4 Flash + Claude Sonnet 5 |
|
|
62
|
+
| OpenAI (your key) | Yes | GPT-5.6 Luna + GPT-5.6 Terra |
|
|
63
|
+
| Google (your key) | Yes | Gemini 3.8 Flash + Gemini 3.5 Flash |
|
|
64
|
+
| Anthropic, xAI, Groq, Mistral, Z.ai, Ollama, others | Not yet | Pick any model yourself |
|
|
65
|
+
|
|
66
|
+
Auto is offered only where a pairing has been measured doing everyday and difficult jobs well — Jeeves's name is on every result. Everywhere else, the model list says so in one line.
|
|
67
|
+
|
|
68
|
+
## Everyday commands
|
|
69
|
+
|
|
70
|
+
| Type | What it does |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `/model` | Choose a service and a model (or Auto), and set your daily limit |
|
|
73
|
+
| `/keys` | Add, replace or remove keys |
|
|
74
|
+
| `/undo` | Put the project folder back to before your last request |
|
|
75
|
+
| `/ask` | Ask before every change in this project folder again (after choosing "always allow") |
|
|
76
|
+
| `/clear` | Start a fresh conversation |
|
|
77
|
+
| `/address` | Change how Jeeves addresses you |
|
|
78
|
+
| `/verbose` | Show the technical details as well |
|
|
79
|
+
| `/help` | See everything in plain English |
|
|
80
|
+
| `/exit` | Leave, with your terminal exactly as it was |
|
|
81
|
+
|
|
82
|
+
Arrow keys and the trackpad scroll the conversation; `Page Up` / `Page Down` jump a screen; `End` returns to the newest. `Ctrl+R` shows the model's reasoning for the last answer.
|
|
83
|
+
|
|
84
|
+
## Platforms
|
|
21
85
|
|
|
22
|
-
|
|
86
|
+
- **macOS** — fully tried and used every day.
|
|
87
|
+
- **Windows and Linux** — every change is built and tested on both automatically; hands-on use on those machines is coming.
|
|
23
88
|
|
|
24
|
-
|
|
89
|
+
## Coming
|
|
25
90
|
|
|
26
|
-
|
|
91
|
+
- Hands-on checks on Windows and Linux.
|
|
92
|
+
- Auto for more services, as each is proven.
|
|
27
93
|
|
|
28
|
-
|
|
29
|
-
- **Z.ai — GLM Coding Plan.** A flat-rate option for heavy daily use: from $18/month, no per-token billing. Z.ai's endpoint speaks the Anthropic protocol and Jeeves connects to it directly. To use it: subscribe at [z.ai](https://z.ai) if you want the plan, copy your Z.ai API key, then in Jeeves type `/keys`, choose Z.ai, and paste the key. Pick Z.ai in `/model` and choose a GLM model (for example GLM-5.3). Subscribing is optional - Jeeves works fine with OpenRouter alone; this is simply a money-saving option for daily drivers.
|
|
30
|
-
- **Ollama.** Local models, no key needed. Start the Ollama app first.
|
|
94
|
+
## Licence
|
|
31
95
|
|
|
32
|
-
|
|
96
|
+
Free to use, for personal use and for work. All rights reserved — see [LICENSE](LICENSE): you may not copy, change, share or sell Jeeves. Versions up to 0.3.0 were released under the MIT licence. Made by [Tianmu Creations](https://tianmucreations.com). More about Jeeves at [tianmucreations.com/jeeves](https://tianmucreations.com/jeeves/).
|
package/bin/jeeves
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
// Jeeves's add-ons need Node.js 22.12 or newer (execa, ink and the AI SDK need 22;
|
|
3
|
+
// commander needs 22.12). An older Node.js gets a plain message instead of a crash.
|
|
4
|
+
const [major, minor] = process.versions.node.split('.').map(Number);
|
|
5
|
+
if (major < 22 || (major === 22 && minor < 12)) {
|
|
6
|
+
console.error(`Jeeves needs Node.js 22.12 or newer, and this computer has ${process.versions.node}. Download the current version from nodejs.org, install it, then open Jeeves again.`);
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
await import('../dist/index.js');
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// The Auto mode model ids, in a module with no imports so the model catalogue can
|
|
2
|
+
// use them without a dependency loop.
|
|
3
|
+
export const AUTO_MODEL_ID = 'jeeves/auto';
|
|
4
|
+
// Models retire and names change (the worker's name ends in a date), so each role
|
|
5
|
+
// has a short list of replacements, used in order - the first one still in the
|
|
6
|
+
// catalogue with tool support wins. Every id was checked live on 17 Sept 2026.
|
|
7
|
+
// The first choices were measured in the model comparison; the replacements after
|
|
8
|
+
// them were chosen as the nearest measured or same-family alternatives:
|
|
9
|
+
// - worker: DeepSeek V4 Flash (dated name), then its undated name, then GLM 5.3
|
|
10
|
+
// Flash (15 of 15 on the bench on 17 Sept, including 2 of 2 on the calculator job,
|
|
11
|
+
// at a similar low price), then GLM 5.3 (6 of 6 on the hardest jobs, but about 15
|
|
12
|
+
// times the price - the last resort);
|
|
13
|
+
// - expert: Claude Sonnet 5 (6 of 6; its reviews fixed the hard failures), then
|
|
14
|
+
// GLM 5.3 (6 of 6 alone), then Claude Opus 5;
|
|
15
|
+
// - strongest model for the last rung: Claude Opus 5.
|
|
16
|
+
// JEEVES_WORKER_MODEL lets the bench try another worker, as JEEVES_EXPERT_MODEL does the expert.
|
|
17
|
+
export const WORKER_MODELS = [
|
|
18
|
+
...(process.env.JEEVES_WORKER_MODEL ? [process.env.JEEVES_WORKER_MODEL] : []),
|
|
19
|
+
'deepseek/deepseek-v4-flash-0731',
|
|
20
|
+
'deepseek/deepseek-v4-flash',
|
|
21
|
+
'z-ai/glm-5.3-flash',
|
|
22
|
+
'z-ai/glm-5.3',
|
|
23
|
+
];
|
|
24
|
+
export const EXPERT_MODELS = [
|
|
25
|
+
...(process.env.JEEVES_EXPERT_MODEL ? [process.env.JEEVES_EXPERT_MODEL] : []),
|
|
26
|
+
'anthropic/claude-sonnet-5',
|
|
27
|
+
'z-ai/glm-5.3',
|
|
28
|
+
'anthropic/claude-opus-5',
|
|
29
|
+
];
|
|
30
|
+
export const TOP_MODELS = ['anthropic/claude-opus-5'];
|
|
31
|
+
export const AUTO_PROFILES = {
|
|
32
|
+
openrouter: { workers: WORKER_MODELS, experts: EXPERT_MODELS, top: TOP_MODELS },
|
|
33
|
+
// OpenAI with the person's own key. Chosen 18 Sept from public results (GPT-5.6 Luna
|
|
34
|
+
// scores near the top models at a tenth of the price), then measured through
|
|
35
|
+
// OpenRouter: Luna working with Terra as expert passed 6 of 6 of the telling jobs
|
|
36
|
+
// (letter, website, calculator, share tracker) at about 4 cents a job. Replacements:
|
|
37
|
+
// GPT-5.4 mini (OpenAI's small model for tool work) and GPT-5.5 - not measured.
|
|
38
|
+
openai: { workers: ['gpt-5.6-luna', 'gpt-5.4-mini'], experts: ['gpt-5.6-terra', 'gpt-5.5'], top: ['gpt-5.6-sol'] },
|
|
39
|
+
// Google with the person's own key (chosen 18 Sept). Measured through
|
|
40
|
+
// OpenRouter: Gemini 3.8 Flash working with 3.5 Flash as expert passed 4 of 5 of the
|
|
41
|
+
// telling jobs (the failure was a web-search fault since fixed), at 10-14 cents a job -
|
|
42
|
+
// about three times OpenAI's. Replacements (not measured): the Flash "latest" alias and
|
|
43
|
+
// 3.5 Flash; the strongest: 3.1 Pro.
|
|
44
|
+
google: { workers: ['gemini-3.8-flash', 'gemini-flash-latest', 'gemini-3.5-flash'], experts: ['gemini-3.5-flash', 'gemini-3.1-pro-preview'], top: ['gemini-3.1-pro-preview'] },
|
|
45
|
+
};
|
|
46
|
+
export function autoProfile(providerId) {
|
|
47
|
+
return AUTO_PROFILES[providerId] ?? null;
|
|
48
|
+
}
|
|
49
|
+
// First choices, for places that need a name before the catalogue has loaded.
|
|
50
|
+
export const AUTO_WORKER_MODEL = WORKER_MODELS[0];
|
|
51
|
+
export const AUTO_EXPERT_MODEL = EXPERT_MODELS[0];
|
|
52
|
+
export const AUTO_TOP_MODEL = TOP_MODELS[0];
|
|
53
|
+
// The first model in the list that is still in the catalogue and can use tools.
|
|
54
|
+
// Before the catalogue has loaded (or when it could not be loaded) the first choice
|
|
55
|
+
// is used, and a retired model then surfaces as a plain "not available" message.
|
|
56
|
+
export function firstAvailable(candidates, catalogue) {
|
|
57
|
+
if (catalogue.length === 0)
|
|
58
|
+
return candidates[0] ?? null;
|
|
59
|
+
for (const id of candidates) {
|
|
60
|
+
const model = catalogue.find((entry) => entry.id === id);
|
|
61
|
+
const params = model?.supportedParameters ?? [];
|
|
62
|
+
if (model && (params.includes('tools') || params.includes('tool_choice')))
|
|
63
|
+
return id;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { session } from '../state/session.js';
|
|
4
|
+
import { expertChat } from './expert-chat.js';
|
|
5
|
+
import { seenModels } from '../providers/catalogue.js';
|
|
6
|
+
// Auto mode: a cheap worker with an expert on call - the pattern Claude Code
|
|
7
|
+
// publishes as its "advisor" (code.claude.com/docs/en/advisor): the main model does
|
|
8
|
+
// the routine work and consults a stronger model at decision points (before
|
|
9
|
+
// committing to an approach, when an error keeps recurring, before declaring a task
|
|
10
|
+
// done), which costs less than running the stronger model throughout. No guessing
|
|
11
|
+
// whether a message is "chat" or "work": the cheap model is always first.
|
|
12
|
+
import { AUTO_MODEL_ID, AUTO_WORKER_MODEL, AUTO_PROFILES, autoProfile, firstAvailable } from './auto-ids.js';
|
|
13
|
+
export { AUTO_MODEL_ID, AUTO_WORKER_MODEL };
|
|
14
|
+
// Auto runs on OpenRouter and on the services with a profile in auto-ids.ts (OpenAI).
|
|
15
|
+
export function hasAuto(providerId) {
|
|
16
|
+
return autoProfile(providerId) !== null;
|
|
17
|
+
}
|
|
18
|
+
// The model list Auto chooses from: OpenRouter's catalogue, or the models a company's
|
|
19
|
+
// key can use (loaded when the company is chosen, and at startup).
|
|
20
|
+
export function autoCatalogue(providerId = session.providerId) {
|
|
21
|
+
return providerId === 'openrouter' ? session.models : seenModels(providerId);
|
|
22
|
+
}
|
|
23
|
+
// A service without Auto falls back to OpenRouter's choices (used by web research).
|
|
24
|
+
function profileFor(providerId) {
|
|
25
|
+
const profile = autoProfile(providerId);
|
|
26
|
+
return profile ? { profile, catalogue: autoCatalogue(providerId) } : { profile: AUTO_PROFILES.openrouter, catalogue: session.models };
|
|
27
|
+
}
|
|
28
|
+
// The models Auto uses right now, allowing for retired models (see auto-ids.ts).
|
|
29
|
+
export function workerModel(providerId = session.providerId) {
|
|
30
|
+
const { profile, catalogue } = profileFor(providerId);
|
|
31
|
+
return firstAvailable(profile.workers, catalogue) ?? profile.workers[0] ?? AUTO_WORKER_MODEL;
|
|
32
|
+
}
|
|
33
|
+
export function expertModel(providerId = session.providerId) {
|
|
34
|
+
const { profile, catalogue } = profileFor(providerId);
|
|
35
|
+
return firstAvailable(profile.experts, catalogue);
|
|
36
|
+
}
|
|
37
|
+
export function topModel(providerId = session.providerId) {
|
|
38
|
+
const { profile, catalogue } = profileFor(providerId);
|
|
39
|
+
return firstAvailable(profile.top, catalogue);
|
|
40
|
+
}
|
|
41
|
+
// Where Auto is offered, by name, for the note in lists without it.
|
|
42
|
+
export const AUTO_SERVICE_NAMES = { openrouter: 'OpenRouter', openai: 'OpenAI', google: 'Google' };
|
|
43
|
+
// Jeeves only offers Auto where a pairing was proven to do good work (the project's
|
|
44
|
+
// core principle: Jeeves's name carries the blame - docs/SPECIFICATION.md). Elsewhere, one plain line says so.
|
|
45
|
+
export function noAutoNote(providerId, label) {
|
|
46
|
+
if (hasAuto(providerId))
|
|
47
|
+
return null;
|
|
48
|
+
const names = Object.keys(AUTO_PROFILES).map((id) => AUTO_SERVICE_NAMES[id] ?? id);
|
|
49
|
+
const list = names.length > 1 ? `${names.slice(0, -1).join(', ')} or ${names[names.length - 1]}` : names[0];
|
|
50
|
+
return `Auto isn't available with ${label} yet - choose ${list} for Auto.`;
|
|
51
|
+
}
|
|
52
|
+
// The Auto row in a company's model list, when it has Auto and its worker is available.
|
|
53
|
+
export function autoRowFor(providerId, models) {
|
|
54
|
+
const profile = autoProfile(providerId);
|
|
55
|
+
if (!profile || providerId === 'openrouter')
|
|
56
|
+
return null;
|
|
57
|
+
const workerId = firstAvailable(profile.workers, models);
|
|
58
|
+
const worker = models.find((model) => model.id === workerId);
|
|
59
|
+
return worker ? { ...worker, id: AUTO_MODEL_ID, name: 'Auto', priceLabel: 'cheap, expert when needed' } : null;
|
|
60
|
+
}
|
|
61
|
+
export function isAuto(modelId) {
|
|
62
|
+
return modelId === AUTO_MODEL_ID;
|
|
63
|
+
}
|
|
64
|
+
// The real model id behind the selection: in Auto mode, the worker.
|
|
65
|
+
export function workingModelId(modelId) {
|
|
66
|
+
return isAuto(modelId) ? workerModel() : modelId;
|
|
67
|
+
}
|
|
68
|
+
// If the cheap model's tool actions keep failing, the expert takes over for the
|
|
69
|
+
// rest of that job: two failures within the last three steps. A person saying no
|
|
70
|
+
// to a permission is not a failure.
|
|
71
|
+
export function shouldTakeOver(stepFailures) {
|
|
72
|
+
return stepFailures.slice(-3).reduce((sum, failures) => sum + failures, 0) >= 2;
|
|
73
|
+
}
|
|
74
|
+
export { countToolFailures } from '../providers/step-control.js';
|
|
75
|
+
export const EXPERT_INSTRUCTIONS = `You are the expert adviser to an assistant doing a job on a computer for someone with no technical background. You see the conversation so far, including every tool action and its result, then the assistant's question.
|
|
76
|
+
Reply with specific, practical guidance: the approach to take, what to check, and any mistake you can see. Name exact files, commands, or facts where you can. Keep it under 200 words.
|
|
77
|
+
Never guess: if the conversation does not show something, say it needs checking.
|
|
78
|
+
If the assistant is clearly out of its depth, begin your reply with the words TAKE OVER.`;
|
|
79
|
+
export const AUTO_NOTE = `
|
|
80
|
+
|
|
81
|
+
Your Expert
|
|
82
|
+
|
|
83
|
+
You can consult a stronger expert with askExpert. It sees this whole conversation. Consult it:
|
|
84
|
+
- before starting a job that changes more than one file, or needs a plan;
|
|
85
|
+
- when the same error has happened twice;
|
|
86
|
+
- before telling {{ADDRESS}} that a job with several steps is finished - say what you did and how you checked it.
|
|
87
|
+
Do not consult it for conversation, simple questions, or one small change. It costs far more than your own work, so ask one clear question.`;
|
|
88
|
+
// A readable copy of the conversation for the expert. Long tool results are
|
|
89
|
+
// shortened - the expert needs the shape of what happened, not every byte.
|
|
90
|
+
export function conversationForExpert(messages, maxChars = 80_000, perResult = 2_000) {
|
|
91
|
+
const lines = [];
|
|
92
|
+
for (const message of messages) {
|
|
93
|
+
if (typeof message.content === 'string') {
|
|
94
|
+
lines.push(`${message.role.toUpperCase()}: ${message.content}`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
for (const part of message.content) {
|
|
98
|
+
if (part.type === 'text')
|
|
99
|
+
lines.push(`${message.role.toUpperCase()}: ${String(part.text)}`);
|
|
100
|
+
else if (part.type === 'tool-call')
|
|
101
|
+
lines.push(`ACTION ${String(part.toolName)}: ${JSON.stringify(part.input)}`);
|
|
102
|
+
else if (part.type === 'tool-result') {
|
|
103
|
+
const output = part.output;
|
|
104
|
+
const value = typeof output?.value === 'string' ? output.value : JSON.stringify(output?.value ?? '');
|
|
105
|
+
lines.push(`RESULT ${String(part.toolName)}: ${value.length > perResult ? value.slice(0, perResult) + ' …[shortened]' : value}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const text = lines.join('\n');
|
|
110
|
+
return text.length > maxChars ? '…[earlier conversation shortened]\n' + text.slice(-maxChars) : text;
|
|
111
|
+
}
|
|
112
|
+
// On in Auto mode (decided 18 Sept): Auto must be excellent without anyone
|
|
113
|
+
// having to think about models. Targeted to programs and documents (review.ts), it
|
|
114
|
+
// rescued 2 of 3 letters and cost 0.2-2.5 cents per checked job. JEEVES_REVIEW=0 turns
|
|
115
|
+
// it off for testing.
|
|
116
|
+
export const REVIEW_FINISHED_JOBS = process.env.JEEVES_REVIEW !== '0';
|
|
117
|
+
export function newAutoTurnState() {
|
|
118
|
+
return { expertTookOver: false, takeoverStep: -1, askedAboutTop: false, onTopModel: false };
|
|
119
|
+
}
|
|
120
|
+
// How many times the expert's price the strongest model costs, from the catalogue
|
|
121
|
+
// (input prices); null when either is missing.
|
|
122
|
+
export function topModelPriceRatio(models, expertId = expertModel(), topId = topModel()) {
|
|
123
|
+
const expert = models.find((model) => model.id === expertId);
|
|
124
|
+
const top = models.find((model) => model.id === topId);
|
|
125
|
+
if (!expert || !top || expert.promptPrice <= 0)
|
|
126
|
+
return null;
|
|
127
|
+
return top.promptPrice / expert.promptPrice;
|
|
128
|
+
}
|
|
129
|
+
export function topModelQuestion(address, ratio, name = topModelName()) {
|
|
130
|
+
const cost = ratio ? ` It costs about ${Number(ratio.toFixed(1))}× as much as the expert.` : '';
|
|
131
|
+
return `This is proving difficult, ${address}. Shall I try the strongest model (${name}) for this job?${cost} (y/n)`;
|
|
132
|
+
}
|
|
133
|
+
// The strongest model's everyday name ("Claude Opus 5"), from the catalogue.
|
|
134
|
+
export function topModelName(providerId = session.providerId) {
|
|
135
|
+
const id = topModel(providerId);
|
|
136
|
+
const found = autoCatalogue(providerId).find((model) => model.id === id);
|
|
137
|
+
if (found)
|
|
138
|
+
return found.name.replace(/^[A-Za-z][A-Za-z0-9 .-]*: /, '');
|
|
139
|
+
return id ? readableModelName(id) : 'the strongest model';
|
|
140
|
+
}
|
|
141
|
+
// "anthropic/claude-opus-5" -> "Claude Opus 5", "gpt-5.6-sol" -> "GPT 5.6 Sol", for
|
|
142
|
+
// when the catalogue has not loaded.
|
|
143
|
+
export function readableModelName(id) {
|
|
144
|
+
return (id.split('/').pop() ?? id)
|
|
145
|
+
.split('-')
|
|
146
|
+
.map((word) => (word === 'gpt' ? 'GPT' : word.charAt(0).toUpperCase() + word.slice(1)))
|
|
147
|
+
.join(' ');
|
|
148
|
+
}
|
|
149
|
+
export function createAskExpertTool(state) {
|
|
150
|
+
return tool({
|
|
151
|
+
description: 'Consult a stronger expert model about the job in hand. It sees the whole conversation. Use at decision points only (see the rules).',
|
|
152
|
+
inputSchema: z.object({ question: z.string().min(1).describe('One clear question for the expert') }),
|
|
153
|
+
execute: async ({ question }, { messages }) => {
|
|
154
|
+
const expert = expertModel();
|
|
155
|
+
if (!expert)
|
|
156
|
+
return 'No expert model is available right now. Carry on carefully, and tell the person the work could not be double-checked.';
|
|
157
|
+
const lineId = session.addToolLine('askExpert', question.slice(0, 60), 'running');
|
|
158
|
+
session.setActiveModel(expert);
|
|
159
|
+
try {
|
|
160
|
+
const reply = await expertChat(expert, [
|
|
161
|
+
{ role: 'system', content: EXPERT_INSTRUCTIONS },
|
|
162
|
+
{ role: 'user', content: `Conversation so far:\n${conversationForExpert(messages)}\n\nThe assistant asks: ${question}` },
|
|
163
|
+
], 1500);
|
|
164
|
+
if (reply.trimStart().toUpperCase().startsWith('TAKE OVER'))
|
|
165
|
+
state.expertTookOver = true;
|
|
166
|
+
session.updateToolLine(lineId, { state: 'done', label: 'Checked with the expert' });
|
|
167
|
+
return reply || 'The expert had no advice to add.';
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
session.updateToolLine(lineId, { state: 'failed', label: 'the expert was not available' });
|
|
171
|
+
return `The expert could not be reached (${error instanceof Error ? error.message : String(error)}). Carry on carefully.`;
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
session.setActiveModel(state.expertTookOver ? expert : workerModel());
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
}
|
package/dist/agent/context.js
CHANGED
|
@@ -1,35 +1,77 @@
|
|
|
1
1
|
import { session } from '../state/session.js';
|
|
2
2
|
import { getActiveProvider } from '../providers/index.js';
|
|
3
|
+
import { getSystemPrompt } from './systemPrompt.js';
|
|
4
|
+
import { DEFAULT_CONTEXT_TOKENS } from '../state/session.js';
|
|
5
|
+
import { ZAI_MODELS } from '../providers/zai.js';
|
|
6
|
+
import { findSeenModel } from '../providers/catalogue.js';
|
|
7
|
+
import { SUMMARY_TRIGGER_TOKENS, SUMMARY_INSTRUCTIONS } from './housekeeping.js';
|
|
8
|
+
import { workerModel, workingModelId } from './auto.js';
|
|
9
|
+
import { reportStepCost } from './spending.js';
|
|
10
|
+
// The model's identity and rulebook. AI SDK 7 rejects role:'system' messages in the
|
|
11
|
+
// messages array ("Use the instructions option instead"), so the prompt travels as
|
|
12
|
+
// streamText's instructions option via the provider layer (see loop.ts).
|
|
13
|
+
export { getSystemPrompt };
|
|
3
14
|
// Builds the message list for one turn; automatic summarisation of older turns (spec 3.3)
|
|
4
15
|
// is deferred until long-conversation handling lands.
|
|
5
16
|
export function buildTurnMessages(history, userText) {
|
|
6
17
|
const userMessage = { role: 'user', content: userText };
|
|
7
18
|
return [...history, userMessage];
|
|
8
19
|
}
|
|
9
|
-
//
|
|
10
|
-
//
|
|
20
|
+
// How much the current model can hold in mind, from the model lists (OpenRouter's
|
|
21
|
+
// catalogue, Z.ai's own list, or a direct connection's); the long-standing default when it isn't listed.
|
|
22
|
+
export function contextLimitFor(modelId, models) {
|
|
23
|
+
const found = models.find((model) => model.id === modelId) ??
|
|
24
|
+
ZAI_MODELS.find((model) => model.id === modelId) ??
|
|
25
|
+
findSeenModel(session.providerId, modelId);
|
|
26
|
+
return found && found.contextLength > 0 ? found.contextLength : DEFAULT_CONTEXT_TOKENS;
|
|
27
|
+
}
|
|
28
|
+
// The conversation is summarised automatically once it fills this share of the
|
|
29
|
+
// model's memory, so the user never has to watch a memory gauge. Matches the
|
|
30
|
+
// amber threshold the old info bar used for "getting full".
|
|
31
|
+
export const AUTO_SUMMARISE_AT = 0.7;
|
|
32
|
+
// Whichever comes first: Anthropic's default compaction point (100,000 tokens), or
|
|
33
|
+
// 70% of the model's memory for models with a small one.
|
|
34
|
+
export function shouldAutoSummarise(conversationTokens, limit) {
|
|
35
|
+
return conversationTokens >= Math.min(SUMMARY_TRIGGER_TOKENS, AUTO_SUMMARISE_AT * limit);
|
|
36
|
+
}
|
|
37
|
+
// A failed summary is never announced - the user could do nothing about it. It is
|
|
38
|
+
// retried quietly, but only after a few messages, so a service that keeps failing
|
|
39
|
+
// is not asked (and billed) for a summary on every single message.
|
|
40
|
+
export const SUMMARY_RETRY_AFTER = 3;
|
|
41
|
+
let messagesUntilRetry = 0;
|
|
42
|
+
export function summaryDue(conversationTokens, limit) {
|
|
43
|
+
if (messagesUntilRetry > 0) {
|
|
44
|
+
messagesUntilRetry -= 1;
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return shouldAutoSummarise(conversationTokens, limit);
|
|
48
|
+
}
|
|
49
|
+
// Condenses the whole conversation into a single summary message so a model can
|
|
50
|
+
// continue without re-reading every turn: on a model switch, or automatically when
|
|
51
|
+
// the conversation grows long.
|
|
11
52
|
export async function summariseHistory() {
|
|
12
53
|
if (session.history.length === 0)
|
|
13
54
|
return;
|
|
14
55
|
session.setStatus('working');
|
|
56
|
+
session.setTidying(true);
|
|
15
57
|
try {
|
|
16
58
|
const provider = getActiveProvider();
|
|
17
|
-
const messages = [
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
role: 'user',
|
|
21
|
-
content: 'Summarise this conversation so far as compact context a model can continue from. Reply with only the summary.',
|
|
22
|
-
},
|
|
23
|
-
];
|
|
59
|
+
const messages = [...session.history, { role: 'user', content: SUMMARY_INSTRUCTIONS }];
|
|
60
|
+
// On OpenRouter the summary is written by the cheap worker model whatever model
|
|
61
|
+
// is selected - summarising needs care, not the most expensive model.
|
|
24
62
|
const result = await provider.stream({
|
|
25
|
-
modelId: session.model,
|
|
63
|
+
modelId: session.providerId === 'openrouter' ? workerModel() : workingModelId(session.model),
|
|
26
64
|
messages,
|
|
27
65
|
tools: {},
|
|
66
|
+
instructions: getSystemPrompt(),
|
|
28
67
|
onToken: () => { },
|
|
29
68
|
onReasoning: () => { },
|
|
30
69
|
onToolCall: () => { },
|
|
31
70
|
});
|
|
71
|
+
for (const cost of result.stepCosts ?? [])
|
|
72
|
+
reportStepCost(cost);
|
|
32
73
|
const summary = result.text.trim();
|
|
74
|
+
// Silent either way: the user can do nothing about it (a failure retries later).
|
|
33
75
|
if (summary) {
|
|
34
76
|
session.setHistory([
|
|
35
77
|
{
|
|
@@ -37,14 +79,14 @@ export async function summariseHistory() {
|
|
|
37
79
|
content: `A summary of the conversation so far:\n\n${summary}\n\nContinue helping from this point.`,
|
|
38
80
|
},
|
|
39
81
|
]);
|
|
40
|
-
session.addNotice('Conversation summarised for the new model.');
|
|
41
82
|
}
|
|
42
83
|
else {
|
|
43
|
-
|
|
84
|
+
messagesUntilRetry = SUMMARY_RETRY_AFTER;
|
|
44
85
|
}
|
|
45
86
|
}
|
|
46
87
|
catch {
|
|
47
|
-
|
|
88
|
+
messagesUntilRetry = SUMMARY_RETRY_AFTER;
|
|
48
89
|
}
|
|
90
|
+
session.setTidying(false);
|
|
49
91
|
session.setStatus('idle');
|
|
50
92
|
}
|