@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.
Files changed (57) hide show
  1. package/LICENSE +37 -17
  2. package/README.md +82 -18
  3. package/bin/jeeves +8 -1
  4. package/dist/agent/auto-ids.js +66 -0
  5. package/dist/agent/auto.js +178 -0
  6. package/dist/agent/context.js +55 -13
  7. package/dist/agent/errors.js +83 -22
  8. package/dist/agent/expert-chat.js +33 -0
  9. package/dist/agent/housekeeping.js +55 -0
  10. package/dist/agent/loop.js +174 -12
  11. package/dist/agent/permissions.js +186 -3
  12. package/dist/agent/research-gate.js +267 -0
  13. package/dist/agent/review.js +135 -0
  14. package/dist/agent/spending.js +73 -0
  15. package/dist/agent/systemPrompt.js +112 -0
  16. package/dist/agent/trust.js +29 -0
  17. package/dist/app.js +31 -11
  18. package/dist/checkpoints/index.js +103 -0
  19. package/dist/checkpoints/store.js +239 -0
  20. package/dist/commands/address.js +5 -0
  21. package/dist/commands/clear.js +2 -0
  22. package/dist/commands/help.js +8 -4
  23. package/dist/commands/keys.js +1 -1
  24. package/dist/commands/verbose.js +1 -1
  25. package/dist/components/AddressPrompt.js +31 -0
  26. package/dist/components/Footer.js +74 -102
  27. package/dist/components/Input.js +115 -29
  28. package/dist/components/KeysManager.js +65 -20
  29. package/dist/components/ModelPicker.js +348 -75
  30. package/dist/components/ProjectPicker.js +4 -1
  31. package/dist/components/Transcript.js +29 -14
  32. package/dist/components/input-layout.js +92 -0
  33. package/dist/components/transcript-layout.js +27 -19
  34. package/dist/index.js +25 -7
  35. package/dist/ink/AlternateScreen.js +33 -16
  36. package/dist/ink/cursor.js +18 -0
  37. package/dist/ink/mouse.js +48 -0
  38. package/dist/keys/store.js +2 -1
  39. package/dist/models/registry.js +18 -2
  40. package/dist/platform/config.js +71 -7
  41. package/dist/providers/catalogue.js +293 -0
  42. package/dist/providers/direct-services.js +65 -0
  43. package/dist/providers/direct.js +145 -0
  44. package/dist/providers/index.js +123 -13
  45. package/dist/providers/models-snapshot.js +1037 -0
  46. package/dist/providers/ollama.js +21 -4
  47. package/dist/providers/openrouter.js +39 -4
  48. package/dist/providers/step-control.js +28 -0
  49. package/dist/providers/zai.js +31 -11
  50. package/dist/state/session.js +110 -36
  51. package/dist/state/today-spend.js +26 -0
  52. package/dist/tools/index.js +123 -11
  53. package/dist/tools/runBash.js +58 -11
  54. package/dist/tools/web/htmlToText.js +32 -0
  55. package/dist/tools/web/openrouterChat.js +31 -0
  56. package/dist/tools/web/research.js +191 -0
  57. package/package.json +33 -7
package/LICENSE CHANGED
@@ -1,21 +1,41 @@
1
- MIT License
1
+ Jeeves Licence
2
+ Copyright (c) 2026 Tianmu Creations. All rights reserved.
2
3
 
3
- Copyright (c) 2026 tianmucreations
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
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
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
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
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
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
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
- A plain-English terminal assistant: describe what you need in ordinary words and it reads files, writes files, lists folders, and runs commands for you - asking permission before anything that changes your computer.
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
- ## Platforms
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
- Designed and built for **macOS, Windows, and Linux** from a single codebase.
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
- - macOS is fully verified.
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
- ## Quick start (local development)
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
- 1. Install Node.js 20 or later.
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
- On first launch you are asked for an OpenRouter API key, which is stored securely in your operating system's credential store (macOS Keychain, Windows Credential Vault, Linux Secret Service) - never in a plain file.
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
- ## Keyboard
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
- Everything is visible on screen: arrow keys move, Enter selects, Esc goes back. `Tab` zooms a footer metric. `Ctrl+R` reveals the model's last reasoning. Slash commands: `/model`, `/keys`, `/verbose`.
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
- Jeeves takes over the whole terminal window (the same way vim does). The shell's own scrollback is unavailable while it runs, so the up and down arrows scroll the conversation instead - `Page Up` / `Page Down` jump a whole screen. When you quit, the terminal returns exactly as it was.
89
+ ## Coming
25
90
 
26
- ## Providers
91
+ - Hands-on checks on Windows and Linux.
92
+ - Auto for more services, as each is proven.
27
93
 
28
- - **OpenRouter (default).** One key unlocks 400+ models from every major provider. Get a key at [openrouter.ai/settings/keys](https://openrouter.ai/settings/keys). Repeat conversation context is cached (sticky routing), so long conversations cost a fraction of the fresh-token price.
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
- API keys are stored in your operating system's credential store - never in a plain file.
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
- import '../dist/index.js';
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
+ }
@@ -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
- // Condenses the whole conversation into a single summary message so a newly
10
- // selected model can continue without re-reading every turn.
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
- ...session.history,
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
- session.addNotice('Could not summarise - kept the conversation as-is.');
84
+ messagesUntilRetry = SUMMARY_RETRY_AFTER;
44
85
  }
45
86
  }
46
87
  catch {
47
- session.addNotice('Could not summarise - kept the conversation as-is.');
88
+ messagesUntilRetry = SUMMARY_RETRY_AFTER;
48
89
  }
90
+ session.setTidying(false);
49
91
  session.setStatus('idle');
50
92
  }