@oharness/engine 0.9.3 → 0.11.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/dist/acp/claude.d.ts +34 -0
- package/dist/acp/claude.js +122 -0
- package/dist/acp/claude.js.map +1 -0
- package/dist/acp/client.d.ts +193 -0
- package/dist/acp/client.js +279 -0
- package/dist/acp/client.js.map +1 -0
- package/dist/acp/runner.d.ts +80 -0
- package/dist/acp/runner.js +159 -0
- package/dist/acp/runner.js.map +1 -0
- package/dist/agent/agent.d.ts +258 -2
- package/dist/agent/agent.js +1010 -53
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/background.d.ts +135 -0
- package/dist/agent/background.js +307 -0
- package/dist/agent/background.js.map +1 -0
- package/dist/agent/compaction.d.ts +7 -0
- package/dist/agent/compaction.js +11 -3
- package/dist/agent/compaction.js.map +1 -1
- package/dist/agent/goal.d.ts +36 -0
- package/dist/agent/goal.js +71 -0
- package/dist/agent/goal.js.map +1 -0
- package/dist/agent/hooks.js +6 -6
- package/dist/agent/hooks.js.map +1 -1
- package/dist/agent/permissions.d.ts +6 -0
- package/dist/agent/permissions.js +2 -1
- package/dist/agent/permissions.js.map +1 -1
- package/dist/agent/session.d.ts +38 -0
- package/dist/agent/session.js +91 -2
- package/dist/agent/session.js.map +1 -1
- package/dist/agent/store.d.ts +25 -0
- package/dist/agent/store.js +21 -0
- package/dist/agent/store.js.map +1 -1
- package/dist/agent/subagent.d.ts +21 -0
- package/dist/agent/subagent.js +17 -1
- package/dist/agent/subagent.js.map +1 -1
- package/dist/agent/workspace.d.ts +9 -0
- package/dist/agent/workspace.js +59 -5
- package/dist/agent/workspace.js.map +1 -1
- package/dist/auth/index.d.ts +3 -1
- package/dist/auth/index.js +3 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/manager.d.ts +9 -0
- package/dist/auth/manager.js +71 -6
- package/dist/auth/manager.js.map +1 -1
- package/dist/auth/oauth.d.ts +33 -0
- package/dist/auth/oauth.js +59 -8
- package/dist/auth/oauth.js.map +1 -1
- package/dist/auth/providers/account.js +5 -4
- package/dist/auth/providers/account.js.map +1 -1
- package/dist/auth/providers/anthropic.d.ts +25 -5
- package/dist/auth/providers/anthropic.js +47 -6
- package/dist/auth/providers/anthropic.js.map +1 -1
- package/dist/auth/providers/chatgpt.d.ts +8 -0
- package/dist/auth/providers/chatgpt.js +180 -0
- package/dist/auth/providers/chatgpt.js.map +1 -0
- package/dist/auth/providers/openrouter.d.ts +2 -0
- package/dist/auth/providers/openrouter.js +101 -0
- package/dist/auth/providers/openrouter.js.map +1 -0
- package/dist/auth/types.d.ts +15 -0
- package/dist/auth/types.js.map +1 -1
- package/dist/browser/index.d.ts +1 -1
- package/dist/browser/plugin.d.ts +6 -1
- package/dist/browser/plugin.js.map +1 -1
- package/dist/browser/session.d.ts +47 -0
- package/dist/browser/session.js +177 -2
- package/dist/browser/session.js.map +1 -1
- package/dist/browser/tools.js +142 -18
- package/dist/browser/tools.js.map +1 -1
- package/dist/computer/arbiter.d.ts +69 -0
- package/dist/computer/arbiter.js +128 -0
- package/dist/computer/arbiter.js.map +1 -0
- package/dist/computer/cua-driver.d.ts +174 -0
- package/dist/computer/cua-driver.js +597 -0
- package/dist/computer/cua-driver.js.map +1 -0
- package/dist/computer/helper-client.d.ts +80 -0
- package/dist/computer/helper-client.js +166 -0
- package/dist/computer/helper-client.js.map +1 -0
- package/dist/computer/index.d.ts +16 -0
- package/dist/computer/index.js +9 -0
- package/dist/computer/index.js.map +1 -0
- package/dist/computer/mode.d.ts +34 -0
- package/dist/computer/mode.js +52 -0
- package/dist/computer/mode.js.map +1 -0
- package/dist/computer/plugin.d.ts +24 -0
- package/dist/computer/plugin.js +17 -0
- package/dist/computer/plugin.js.map +1 -0
- package/dist/computer/policy.d.ts +29 -0
- package/dist/computer/policy.js +127 -0
- package/dist/computer/policy.js.map +1 -0
- package/dist/computer/session.d.ts +228 -0
- package/dist/computer/session.js +493 -0
- package/dist/computer/session.js.map +1 -0
- package/dist/computer/target.d.ts +206 -0
- package/dist/computer/target.js +31 -0
- package/dist/computer/target.js.map +1 -0
- package/dist/computer/tools.d.ts +8 -0
- package/dist/computer/tools.js +664 -0
- package/dist/computer/tools.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js.map +1 -1
- package/dist/core/tokens.js +4 -0
- package/dist/core/tokens.js.map +1 -1
- package/dist/core/types.d.ts +16 -1
- package/dist/core/types.js.map +1 -1
- package/dist/harness.d.ts +128 -1
- package/dist/harness.js +287 -30
- package/dist/harness.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/manager.d.ts +15 -0
- package/dist/mcp/manager.js +28 -1
- package/dist/mcp/manager.js.map +1 -1
- package/dist/mcp/transport.js +3 -0
- package/dist/mcp/transport.js.map +1 -1
- package/dist/providers/catalog.js +6 -0
- package/dist/providers/catalog.js.map +1 -1
- package/dist/providers/catalog.json +14 -3
- package/dist/providers/discovery.js +10 -2
- package/dist/providers/discovery.js.map +1 -1
- package/dist/providers/models-dev.d.ts +15 -0
- package/dist/providers/models-dev.js +15 -0
- package/dist/providers/models-dev.js.map +1 -1
- package/dist/providers/models-dev.json +114 -102
- package/dist/providers/piai.d.ts +54 -2
- package/dist/providers/piai.js +154 -9
- package/dist/providers/piai.js.map +1 -1
- package/dist/providers/registry.d.ts +16 -1
- package/dist/providers/registry.js +42 -11
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/types.d.ts +6 -0
- package/dist/providers/types.js.map +1 -1
- package/dist/runtime-plugins/builtin/studio.d.ts +7 -0
- package/dist/runtime-plugins/builtin/studio.js +69 -4
- package/dist/runtime-plugins/builtin/studio.js.map +1 -1
- package/dist/runtime-plugins/cloud-connector.js +5 -0
- package/dist/runtime-plugins/cloud-connector.js.map +1 -1
- package/dist/schedule/scheduler.d.ts +38 -0
- package/dist/schedule/scheduler.js +101 -1
- package/dist/schedule/scheduler.js.map +1 -1
- package/dist/tools/builtin/bash.d.ts +6 -0
- package/dist/tools/builtin/bash.js +8 -11
- package/dist/tools/builtin/bash.js.map +1 -1
- package/dist/tools/builtin/recall.js +4 -0
- package/dist/tools/builtin/recall.js.map +1 -1
- package/dist/tools/builtin/routine.d.ts +9 -0
- package/dist/tools/builtin/routine.js +23 -2
- package/dist/tools/builtin/routine.js.map +1 -1
- package/dist/tools/builtin/task-control.d.ts +21 -0
- package/dist/tools/builtin/task-control.js +120 -0
- package/dist/tools/builtin/task-control.js.map +1 -0
- package/dist/tools/builtin/task.js +50 -9
- package/dist/tools/builtin/task.js.map +1 -1
- package/dist/tools/builtin/todo.d.ts +1 -1
- package/dist/tools/builtin/worktree.d.ts +10 -0
- package/dist/tools/builtin/worktree.js +52 -0
- package/dist/tools/builtin/worktree.js.map +1 -0
- package/dist/tools/define.d.ts +4 -1
- package/dist/tools/define.js +2 -0
- package/dist/tools/define.js.map +1 -1
- package/dist/tools/types.d.ts +112 -0
- package/dist/tools/types.js.map +1 -1
- package/dist/util/shell.d.ts +30 -0
- package/dist/util/shell.js +70 -0
- package/dist/util/shell.js.map +1 -1
- package/dist/worktree/index.d.ts +109 -0
- package/dist/worktree/index.js +463 -0
- package/dist/worktree/index.js.map +1 -0
- package/package.json +3 -3
- package/skills/media-generation/SKILL.md +22 -1
package/dist/harness.js
CHANGED
|
@@ -6,6 +6,7 @@ import { HookRegistry } from "./agent/hooks.js";
|
|
|
6
6
|
import { Session } from "./agent/session.js";
|
|
7
7
|
import { SessionStore } from "./agent/store.js";
|
|
8
8
|
import { BUILTIN_SUBAGENTS, SubagentRegistry, loadMarkdownSubagents } from "./agent/subagent.js";
|
|
9
|
+
import { claudeAgent } from "./acp/claude.js";
|
|
9
10
|
import { SkillRegistry } from "./agent/skills.js";
|
|
10
11
|
import { SettingsStore } from "./agent/settings.js";
|
|
11
12
|
import { createPolicy } from "./agent/workspace.js";
|
|
@@ -60,10 +61,40 @@ export class Harness {
|
|
|
60
61
|
extensions = new PluginExtensions();
|
|
61
62
|
/** Undefined when browser tools are turned off. Lazy: holds no process until used. */
|
|
62
63
|
browser;
|
|
64
|
+
/**
|
|
65
|
+
* Who hosts a session started in a new worktree. Set by the frontend that
|
|
66
|
+
* holds sessions (the web server); while unset, `worktree_start` is not
|
|
67
|
+
* offered to the model.
|
|
68
|
+
*/
|
|
69
|
+
worktreeLauncher;
|
|
70
|
+
/**
|
|
71
|
+
* Starts a turn in a conversation that is idle, to read in background task
|
|
72
|
+
* results that ended after its last run. Set by a frontend that keeps
|
|
73
|
+
* conversations alive between turns; while unset, background tasks are not
|
|
74
|
+
* offered and `task` runs everything in the foreground.
|
|
75
|
+
*/
|
|
76
|
+
taskWaker;
|
|
77
|
+
/**
|
|
78
|
+
* Where pictures of the browser tabs a turn left go, and whether to take
|
|
79
|
+
* them at all. Set by a frontend that shows them; while unset, none are.
|
|
80
|
+
* `at` is the conversation's message count when the turn ended.
|
|
81
|
+
*/
|
|
82
|
+
turnShotSink;
|
|
63
83
|
/** Browser state belongs to a conversation, not to its project harness. */
|
|
64
84
|
sessionBrowsers = new Map();
|
|
85
|
+
/** Computer Use state belongs to a conversation too. */
|
|
86
|
+
sessionComputers = new Map();
|
|
65
87
|
/** Each live conversation's title, read when its browser needs a name. */
|
|
66
88
|
sessionTitles = new Map();
|
|
89
|
+
/** Each live conversation's model, read when it creates a routine. */
|
|
90
|
+
sessionModels = new Map();
|
|
91
|
+
/**
|
|
92
|
+
* The model a routine runs on when it names none of its own, or names one
|
|
93
|
+
* that is gone. Set by whoever knows what the user picked — the app's
|
|
94
|
+
* composer — because the first-configured model is a guess: a local server
|
|
95
|
+
* that is not running, or a gated one the account never unlocked.
|
|
96
|
+
*/
|
|
97
|
+
routineModel;
|
|
67
98
|
memoryService;
|
|
68
99
|
/** User-level notes, shared by every project. Absent when the user directory is not writable. */
|
|
69
100
|
memoryUserService;
|
|
@@ -183,6 +214,8 @@ export class Harness {
|
|
|
183
214
|
list: () => this.scheduler.status(),
|
|
184
215
|
add: (input) => this.addSchedule(input),
|
|
185
216
|
remove: (name) => this.removeSchedule(name),
|
|
217
|
+
sessionModel: (sessionId) => this.sessionModels.get(sessionId)?.(),
|
|
218
|
+
appStatus: (app, signal) => this.appStatus(app, signal),
|
|
186
219
|
}));
|
|
187
220
|
this.ephemeral = opts.ephemeral === true;
|
|
188
221
|
this.runtimeAudit = new RuntimeAuditBus(this.logger);
|
|
@@ -237,6 +270,11 @@ export class Harness {
|
|
|
237
270
|
.catch((err) => harness.logger.warn("session pruning failed", { error: String(err) }));
|
|
238
271
|
}
|
|
239
272
|
await harness.skills.discover(cwd, config.skillDirs ?? []);
|
|
273
|
+
// Claude Agent, when the user turned it on and this machine has their
|
|
274
|
+
// own Claude Code to hand work to. Before the markdown ones, so a
|
|
275
|
+
// project can redefine it.
|
|
276
|
+
if (config.claudeAgent === true)
|
|
277
|
+
await harness.registerClaudeAgent();
|
|
240
278
|
// Markdown definitions layer over the JSON ones, so a repo can drop in an
|
|
241
279
|
// agent file without editing config.
|
|
242
280
|
for (const definition of await loadMarkdownSubagents(cwd, config.agentDirs ?? [])) {
|
|
@@ -283,19 +321,7 @@ export class Harness {
|
|
|
283
321
|
}
|
|
284
322
|
}, token: () => harness.accountToken(), syncUrl: () => resolveSyncUrl(harness.config.sync),
|
|
285
323
|
// Merge proposals use the small model, like extraction and titles.
|
|
286
|
-
generate:
|
|
287
|
-
const model = harness.providers.resolve(harness.config.smallModel ?? harness.config.model ?? harness.defaultModel());
|
|
288
|
-
const provider = harness.providers.getProvider(model.provider);
|
|
289
|
-
const parts = [];
|
|
290
|
-
for await (const event of provider.stream({
|
|
291
|
-
model, system, messages: [userMessage(prompt)],
|
|
292
|
-
maxOutputTokens: Math.min(8_000, model.maxOutputTokens), thinking: { enabled: false, effort: "low" },
|
|
293
|
-
...(signal ? { signal } : {}),
|
|
294
|
-
}))
|
|
295
|
-
if (event.type === "text_delta")
|
|
296
|
-
parts.push(event.text);
|
|
297
|
-
return parts.join("");
|
|
298
|
-
},
|
|
324
|
+
generate: (system, prompt, signal) => harness.generateText(system, prompt, signal),
|
|
299
325
|
}, (service, user) => { harness.memoryService = service; harness.memoryUserService = user; });
|
|
300
326
|
/**
|
|
301
327
|
* Media generation, billed to the OHarness account and nothing else.
|
|
@@ -408,6 +434,50 @@ export class Harness {
|
|
|
408
434
|
* started with the browser off has no tools to switch, so that one stays as
|
|
409
435
|
* it is until a restart.
|
|
410
436
|
*/
|
|
437
|
+
/** The Claude Agent definition this harness registered, while it is on. */
|
|
438
|
+
claudeRegistered;
|
|
439
|
+
/**
|
|
440
|
+
* Whether Claude Agent is switched on, and whether it could be.
|
|
441
|
+
*
|
|
442
|
+
* `available` is whether this machine has the user's `claude` (and `npx`
|
|
443
|
+
* for the adapter); `unavailable` is the reason it would not run even so,
|
|
444
|
+
* such as an old Node. Detected afresh each time, since installing Claude
|
|
445
|
+
* Code is something done in another window while this page is open.
|
|
446
|
+
*/
|
|
447
|
+
async claudeAgentStatus() {
|
|
448
|
+
const found = await claudeAgent().catch(() => undefined);
|
|
449
|
+
return {
|
|
450
|
+
on: this.config.claudeAgent === true,
|
|
451
|
+
available: Boolean(found),
|
|
452
|
+
...(found?.runtime?.unavailable ? { unavailable: found.runtime.unavailable } : {}),
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Turn Claude Agent on or off, now and for later launches.
|
|
457
|
+
*
|
|
458
|
+
* Written to the user's own config, the same place `claudeAgent` is read
|
|
459
|
+
* from at startup, so the terminal frontends and the page agree. Takes
|
|
460
|
+
* effect at once: open conversations share this harness's subagent list.
|
|
461
|
+
*/
|
|
462
|
+
async setClaudeAgent(on) {
|
|
463
|
+
await updateUserConfig((current) => ({ ...current, claudeAgent: on }));
|
|
464
|
+
this.config.claudeAgent = on;
|
|
465
|
+
if (on)
|
|
466
|
+
await this.registerClaudeAgent();
|
|
467
|
+
else if (this.claudeRegistered) {
|
|
468
|
+
this.subagents.unregister(this.claudeRegistered);
|
|
469
|
+
this.claudeRegistered = undefined;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async registerClaudeAgent() {
|
|
473
|
+
if (this.claudeRegistered || process.env["OHARNESS_NO_CLAUDE_AGENT"] === "1")
|
|
474
|
+
return;
|
|
475
|
+
const claude = await claudeAgent().catch(() => undefined);
|
|
476
|
+
if (!claude)
|
|
477
|
+
return;
|
|
478
|
+
this.subagents.register(claude);
|
|
479
|
+
this.claudeRegistered = claude;
|
|
480
|
+
}
|
|
411
481
|
async setBrowserMode(mode) {
|
|
412
482
|
await updateUserConfig((current) => ({ ...current, browser: { ...current.browser, mode } }));
|
|
413
483
|
this.config.browser = { ...this.config.browser, mode };
|
|
@@ -543,11 +613,41 @@ export class Harness {
|
|
|
543
613
|
const where = await stat(cwd).catch(() => undefined);
|
|
544
614
|
if (!where?.isDirectory())
|
|
545
615
|
throw new ConfigError(`"${cwd}" is not a directory`);
|
|
546
|
-
const
|
|
616
|
+
const apps = [...new Set((input.apps ?? []).map((app) => app.trim().toLowerCase()).filter(Boolean))];
|
|
617
|
+
const def = {
|
|
618
|
+
name, cron, prompt, cwd, createdAt: Date.now(),
|
|
619
|
+
...(input.model ? { model: input.model } : {}),
|
|
620
|
+
...(apps.length ? { apps } : {}),
|
|
621
|
+
};
|
|
547
622
|
await updateUserConfig((current) => ({ ...current, schedules: [...(current.schedules ?? []), def] }));
|
|
548
623
|
this.scheduler.add(def);
|
|
549
624
|
return def;
|
|
550
625
|
}
|
|
626
|
+
/**
|
|
627
|
+
* The model a routine should run on: its own if that still resolves, else
|
|
628
|
+
* the one `routineModel` names, else nothing — and the session default.
|
|
629
|
+
*/
|
|
630
|
+
routineModelFor(own) {
|
|
631
|
+
for (const ref of [own, this.routineModel?.()]) {
|
|
632
|
+
if (!ref)
|
|
633
|
+
continue;
|
|
634
|
+
try {
|
|
635
|
+
return this.providers.resolve(ref).id;
|
|
636
|
+
}
|
|
637
|
+
catch {
|
|
638
|
+
this.logger.warn("routine model is not available; trying the next", { ref });
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return undefined;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Whether a Cloud app is connected for this account, asked without opening
|
|
645
|
+
* a card. `label` is the app's display name when the Cloud gave one.
|
|
646
|
+
*/
|
|
647
|
+
async appStatus(app, signal) {
|
|
648
|
+
const data = await this.cloudConnectors.call("catalog", { app }, signal);
|
|
649
|
+
return { connected: data.connected !== false, label: typeof data.label === "string" ? data.label : app };
|
|
650
|
+
}
|
|
551
651
|
/**
|
|
552
652
|
* Bring the scheduler in line with the config files as they are now.
|
|
553
653
|
*
|
|
@@ -727,9 +827,9 @@ export class Harness {
|
|
|
727
827
|
* the user catalog so the next run starts with a current list.
|
|
728
828
|
*/
|
|
729
829
|
async refreshModels(providerId) {
|
|
730
|
-
const discovered = await this.providers.
|
|
731
|
-
if (discovered.length > 0) {
|
|
732
|
-
await this.saveDiscovered(discovered).catch((err) => this.logger.warn("could not persist the discovered catalog", {
|
|
830
|
+
const { changed: discovered, cleared } = await this.providers.refreshListings(providerId);
|
|
831
|
+
if (discovered.length > 0 || cleared.length > 0) {
|
|
832
|
+
await this.saveDiscovered(discovered, cleared).catch((err) => this.logger.warn("could not persist the discovered catalog", {
|
|
733
833
|
error: String(err),
|
|
734
834
|
}));
|
|
735
835
|
}
|
|
@@ -744,12 +844,22 @@ export class Harness {
|
|
|
744
844
|
* happen to — and answers 404 when picked. Nothing to refresh from (no
|
|
745
845
|
* listing ever made) counts as stale. Resolves to undefined when the list
|
|
746
846
|
* was fresh enough to leave alone.
|
|
847
|
+
*
|
|
848
|
+
* The age is the oldest provider's, from `discovered`, not `discoveredAt`:
|
|
849
|
+
* that one moves whenever any single provider is listed — a sign-in, a
|
|
850
|
+
* key saved for one vendor — and read alone it kept every other provider's
|
|
851
|
+
* week-old list looking fresh. `discoveredAt` is the fallback for a file
|
|
852
|
+
* written before `discovered` existed.
|
|
747
853
|
*/
|
|
748
854
|
async refreshStaleModels(maxAgeMs = MODEL_LIST_MAX_AGE_MS) {
|
|
749
855
|
let at = NaN;
|
|
750
856
|
try {
|
|
751
857
|
const raw = JSON.parse(await readFile(userCatalogPath(), "utf8"));
|
|
752
|
-
|
|
858
|
+
const each = raw["discovered"];
|
|
859
|
+
const times = each && typeof each === "object" && !Array.isArray(each)
|
|
860
|
+
? Object.values(each).map((value) => Date.parse(String(value))).filter(Number.isFinite)
|
|
861
|
+
: [];
|
|
862
|
+
at = times.length > 0 ? Math.min(...times) : Date.parse(String(raw["discoveredAt"] ?? ""));
|
|
753
863
|
}
|
|
754
864
|
catch {
|
|
755
865
|
// No file or an unreadable one: never listed, so due.
|
|
@@ -784,8 +894,9 @@ export class Harness {
|
|
|
784
894
|
* it: merging only ever grew the file, so a model the vendor withdrew stayed
|
|
785
895
|
* in the picker for good. `discovered` records which providers have a live
|
|
786
896
|
* listing, so the pi-ai snapshot stops filling in models for them.
|
|
897
|
+
* `cleared` names local servers that listed nothing, whose entries go.
|
|
787
898
|
*/
|
|
788
|
-
async saveDiscovered(models) {
|
|
899
|
+
async saveDiscovered(models, cleared = []) {
|
|
789
900
|
const file = userCatalogPath();
|
|
790
901
|
let existing = {};
|
|
791
902
|
try {
|
|
@@ -797,7 +908,7 @@ export class Harness {
|
|
|
797
908
|
const byId = new Map((Array.isArray(existing["models"]) ? existing["models"] : [])
|
|
798
909
|
.filter((m) => !!m && typeof m === "object")
|
|
799
910
|
.map((m) => [String(m["id"]), m]));
|
|
800
|
-
const listed = new Set(models.map((model) => model.provider));
|
|
911
|
+
const listed = new Set([...models.map((model) => model.provider), ...cleared]);
|
|
801
912
|
const fresh = new Set(models.map((model) => model.id));
|
|
802
913
|
for (const [id, model] of byId) {
|
|
803
914
|
const { provider, model: wire } = model;
|
|
@@ -872,6 +983,7 @@ export class Harness {
|
|
|
872
983
|
auditReadyFor;
|
|
873
984
|
equip(session) {
|
|
874
985
|
this.sessionTitles.set(session.id, () => session.title);
|
|
986
|
+
this.sessionModels.set(session.id, () => session.model.id);
|
|
875
987
|
session.permissions.attachSettings(this.settings);
|
|
876
988
|
// Attached whatever the setting says, and the setting read when a decision
|
|
877
989
|
// actually happens. Deciding here instead would make the switch on the
|
|
@@ -930,7 +1042,8 @@ export class Harness {
|
|
|
930
1042
|
*/
|
|
931
1043
|
async forkSession(id, opts = {}) {
|
|
932
1044
|
const forked = await Session.fork(id, {
|
|
933
|
-
model: opts.model ??
|
|
1045
|
+
model: opts.model ?? "",
|
|
1046
|
+
fallbackModel: this.config.model ?? this.defaultModel(),
|
|
934
1047
|
providers: this.providers,
|
|
935
1048
|
permissionMode: this.config.permissionMode,
|
|
936
1049
|
permissionRules: this.config.permissions,
|
|
@@ -959,7 +1072,10 @@ export class Harness {
|
|
|
959
1072
|
/** Reload a persisted session by id; returns undefined if it is unknown. */
|
|
960
1073
|
async resumeSession(id, model) {
|
|
961
1074
|
const resumed = await Session.resume(id, {
|
|
962
|
-
|
|
1075
|
+
// Empty means "the session's own": a conversation reopens on the model
|
|
1076
|
+
// it was using, and the default is only for one whose model is gone.
|
|
1077
|
+
model: model ?? "",
|
|
1078
|
+
fallbackModel: this.config.model ?? this.defaultModel(),
|
|
963
1079
|
providers: this.providers,
|
|
964
1080
|
permissionMode: this.config.permissionMode,
|
|
965
1081
|
permissionRules: this.config.permissions,
|
|
@@ -972,9 +1088,15 @@ export class Harness {
|
|
|
972
1088
|
if (this.lifecycle.signal.aborted)
|
|
973
1089
|
throw new ConfigError("Harness is closed");
|
|
974
1090
|
const browser = this.browserFor(session.id);
|
|
975
|
-
|
|
1091
|
+
let tools = browser && browser !== this.browser
|
|
976
1092
|
? this.tools.clone().registerAll(this.browserUse.tools(browser))
|
|
977
1093
|
: this.tools;
|
|
1094
|
+
// Registered for every conversation and shown only to those that switched
|
|
1095
|
+
// it on; see Agent.visibleTools.
|
|
1096
|
+
const computer = this.computerFor(session.id);
|
|
1097
|
+
if (computer && this.computerUse) {
|
|
1098
|
+
tools = (tools === this.tools ? tools.clone() : tools).registerAll(this.computerUse.tools(computer));
|
|
1099
|
+
}
|
|
978
1100
|
return new Agent({
|
|
979
1101
|
session,
|
|
980
1102
|
tools,
|
|
@@ -998,17 +1120,96 @@ export class Harness {
|
|
|
998
1120
|
cache: this.config.cache,
|
|
999
1121
|
logger: this.logger,
|
|
1000
1122
|
lifecycleSignal: this.lifecycle.signal,
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
return () => { this.activeRuns.delete(done); finish(); };
|
|
1006
|
-
},
|
|
1123
|
+
worktreeLauncher: () => this.worktreeLauncher,
|
|
1124
|
+
taskWaker: () => this.taskWaker,
|
|
1125
|
+
equipSubagent: (sessionId, label, tools) => this.equipSubagent(sessionId, label, tools),
|
|
1126
|
+
onRunStart: () => this.trackRun(computer, browser, session),
|
|
1007
1127
|
onAudit: (event) => { if (this.config.audit !== false)
|
|
1008
1128
|
this.runtimeAudit.publish(event); },
|
|
1009
1129
|
...overrides,
|
|
1010
1130
|
});
|
|
1011
1131
|
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Count a run toward shutdown's drain. Its end lets go of the windows and
|
|
1134
|
+
* control its conversation held, so another need not wait out the lease.
|
|
1135
|
+
*/
|
|
1136
|
+
trackRun(computer, browser, session) {
|
|
1137
|
+
let finish;
|
|
1138
|
+
const done = new Promise((resolve) => { finish = resolve; });
|
|
1139
|
+
this.activeRuns.add(done);
|
|
1140
|
+
return () => {
|
|
1141
|
+
this.activeRuns.delete(done);
|
|
1142
|
+
computer?.endTurn();
|
|
1143
|
+
// A subagent's tabs are not pictured: it has no transcript of its own.
|
|
1144
|
+
if (session)
|
|
1145
|
+
this.pictureBrowserTurn(browser, session);
|
|
1146
|
+
finish();
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* The tabs a turn left, beside the conversation (see BrowserUseSession.endTurn).
|
|
1151
|
+
* Where the turn ended is read now, before a queued message starts the next.
|
|
1152
|
+
*/
|
|
1153
|
+
pictureBrowserTurn(browser, session) {
|
|
1154
|
+
if (!browser?.endTurn)
|
|
1155
|
+
return;
|
|
1156
|
+
const sink = this.turnShotSink;
|
|
1157
|
+
const at = session.messages.length;
|
|
1158
|
+
void browser
|
|
1159
|
+
.endTurn(Boolean(sink?.enabled()))
|
|
1160
|
+
.then((shots) => { if (shots.length)
|
|
1161
|
+
sink?.save(session.id, at, shots); })
|
|
1162
|
+
.catch(() => { });
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* A subagent's own browser and computer.
|
|
1166
|
+
*
|
|
1167
|
+
* Without them it would drive its parent's tabs and act under its parent's
|
|
1168
|
+
* window leases, where the arbiter cannot tell the two apart — and several
|
|
1169
|
+
* subagents at once would take each other's pages away. Only the tools the
|
|
1170
|
+
* subagent was already given are rebound; a filter that left the browser
|
|
1171
|
+
* out leaves it out. Both are closed again when the subagent ends.
|
|
1172
|
+
*/
|
|
1173
|
+
equipSubagent(sessionId, label, tools) {
|
|
1174
|
+
const browser = this.browser
|
|
1175
|
+
? this.browserUse.createSession({
|
|
1176
|
+
sessionId,
|
|
1177
|
+
mode: this.browserMode(),
|
|
1178
|
+
logger: this.logger,
|
|
1179
|
+
label: () => label,
|
|
1180
|
+
cwd: this.cwd,
|
|
1181
|
+
})
|
|
1182
|
+
: undefined;
|
|
1183
|
+
if (browser)
|
|
1184
|
+
for (const tool of this.browserUse.tools(browser))
|
|
1185
|
+
if (tools.has(tool.name))
|
|
1186
|
+
tools.register(tool);
|
|
1187
|
+
const computer = this.options.computerUsePlugin?.createSession({ sessionId, label: () => label });
|
|
1188
|
+
if (computer && this.computerUse) {
|
|
1189
|
+
for (const tool of this.computerUse.tools(computer))
|
|
1190
|
+
if (tools.has(tool.name))
|
|
1191
|
+
tools.register(tool);
|
|
1192
|
+
}
|
|
1193
|
+
return {
|
|
1194
|
+
onRunStart: () => this.trackRun(computer),
|
|
1195
|
+
// A later run's registry gets the same browser and computer again.
|
|
1196
|
+
bind: (next) => {
|
|
1197
|
+
if (browser)
|
|
1198
|
+
for (const tool of this.browserUse.tools(browser))
|
|
1199
|
+
if (next.has(tool.name))
|
|
1200
|
+
next.register(tool);
|
|
1201
|
+
if (computer && this.computerUse) {
|
|
1202
|
+
for (const tool of this.computerUse.tools(computer))
|
|
1203
|
+
if (next.has(tool.name))
|
|
1204
|
+
next.register(tool);
|
|
1205
|
+
}
|
|
1206
|
+
},
|
|
1207
|
+
release: async () => {
|
|
1208
|
+
await computer?.close();
|
|
1209
|
+
await browser?.close();
|
|
1210
|
+
},
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1012
1213
|
/**
|
|
1013
1214
|
* The lazy browser owned by one session.
|
|
1014
1215
|
*
|
|
@@ -1040,11 +1241,35 @@ export class Harness {
|
|
|
1040
1241
|
this.sessionBrowsers.set(sessionId, browser);
|
|
1041
1242
|
return browser;
|
|
1042
1243
|
}
|
|
1244
|
+
/** Whether this harness can offer Computer Use at all. */
|
|
1245
|
+
get computerUse() {
|
|
1246
|
+
return this.options.computerUsePlugin;
|
|
1247
|
+
}
|
|
1248
|
+
/** One conversation's view of the computer; undefined without the feature. */
|
|
1249
|
+
computerFor(sessionId) {
|
|
1250
|
+
const plugin = this.options.computerUsePlugin;
|
|
1251
|
+
if (!plugin)
|
|
1252
|
+
return undefined;
|
|
1253
|
+
let found = this.sessionComputers.get(sessionId);
|
|
1254
|
+
if (!found) {
|
|
1255
|
+
found = plugin.createSession({ sessionId, label: () => this.sessionTitles.get(sessionId)?.() });
|
|
1256
|
+
this.sessionComputers.set(sessionId, found);
|
|
1257
|
+
}
|
|
1258
|
+
return found;
|
|
1259
|
+
}
|
|
1260
|
+
/** Release a closed conversation's leases and its place in the control queue. */
|
|
1261
|
+
async closeComputerFor(sessionId) {
|
|
1262
|
+
const computer = this.sessionComputers.get(sessionId);
|
|
1263
|
+
this.sessionComputers.delete(sessionId);
|
|
1264
|
+
await computer?.close();
|
|
1265
|
+
}
|
|
1043
1266
|
/** Release the process and transient page state when a session tab closes. */
|
|
1044
1267
|
async closeBrowserFor(sessionId) {
|
|
1268
|
+
await this.closeComputerFor(sessionId);
|
|
1045
1269
|
const browser = this.sessionBrowsers.get(sessionId);
|
|
1046
1270
|
this.sessionBrowsers.delete(sessionId);
|
|
1047
1271
|
this.sessionTitles.delete(sessionId);
|
|
1272
|
+
this.sessionModels.delete(sessionId);
|
|
1048
1273
|
await browser?.close();
|
|
1049
1274
|
}
|
|
1050
1275
|
/**
|
|
@@ -1053,7 +1278,8 @@ export class Harness {
|
|
|
1053
1278
|
*/
|
|
1054
1279
|
async *run(prompt, opts = {}) {
|
|
1055
1280
|
const session = this.createSession({ model: opts.model });
|
|
1056
|
-
|
|
1281
|
+
// One prompt and done: nothing would be left to read a late result into.
|
|
1282
|
+
const agent = this.agent(session, { taskWaker: () => undefined });
|
|
1057
1283
|
yield* agent.run(prompt, { signal: opts.signal });
|
|
1058
1284
|
}
|
|
1059
1285
|
close() {
|
|
@@ -1086,11 +1312,42 @@ export class Harness {
|
|
|
1086
1312
|
await this.mcp.closeAll();
|
|
1087
1313
|
// Last, and never awaited for long: a browser that will not close should
|
|
1088
1314
|
// not be what keeps the process alive.
|
|
1315
|
+
for (const computer of this.sessionComputers.values())
|
|
1316
|
+
await computer.close();
|
|
1317
|
+
this.sessionComputers.clear();
|
|
1089
1318
|
const browsers = new Set(this.sessionBrowsers.values());
|
|
1090
1319
|
if (this.browser)
|
|
1091
1320
|
browsers.add(this.browser);
|
|
1092
1321
|
await Promise.all([...browsers].map((browser) => browser.close()));
|
|
1093
1322
|
}
|
|
1323
|
+
/**
|
|
1324
|
+
* One reply from the small model, as text: no tools, no thinking, no
|
|
1325
|
+
* session. For the chores around the work — merge proposals, suggestions —
|
|
1326
|
+
* where the configured default model would be paying for depth nobody reads.
|
|
1327
|
+
* `fallback` is a model the caller knows the user can reach, such as the one
|
|
1328
|
+
* picked in the composer; it wins over the first-configured guess.
|
|
1329
|
+
*/
|
|
1330
|
+
async generateText(system, prompt, signal, maxOutputTokens = 8_000, fallback) {
|
|
1331
|
+
return this.generateWith(this.config.smallModel ?? fallback ?? this.config.model ?? this.defaultModel(), system, prompt, signal, maxOutputTokens);
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* The same one reply, from a model the caller names rather than the small
|
|
1335
|
+
* one: for a chore that should cost and sound like the conversation it is
|
|
1336
|
+
* about, such as the composer's next-message suggestion.
|
|
1337
|
+
*/
|
|
1338
|
+
async generateWith(ref, system, prompt, signal, maxOutputTokens = 8_000) {
|
|
1339
|
+
const model = this.providers.resolve(ref);
|
|
1340
|
+
const provider = this.providers.getProvider(model.provider);
|
|
1341
|
+
const parts = [];
|
|
1342
|
+
for await (const event of provider.stream({
|
|
1343
|
+
model, system, messages: [userMessage(prompt)],
|
|
1344
|
+
maxOutputTokens: Math.min(maxOutputTokens, model.maxOutputTokens), thinking: { enabled: false, effort: "low" },
|
|
1345
|
+
...(signal ? { signal } : {}),
|
|
1346
|
+
}))
|
|
1347
|
+
if (event.type === "text_delta")
|
|
1348
|
+
parts.push(event.text);
|
|
1349
|
+
return parts.join("");
|
|
1350
|
+
}
|
|
1094
1351
|
/**
|
|
1095
1352
|
* First model whose provider has credentials. Only the cheap synchronous
|
|
1096
1353
|
* check runs here — a subscription-only provider is picked up once the
|