hive-am 1.2.2 → 1.4.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/CHANGELOG.md +34 -1
- package/README.md +19 -0
- package/package.json +1 -1
- package/server/dist/context.js +62 -0
- package/server/dist/db/migrations.js +7 -0
- package/server/dist/db.js +4 -4
- package/server/dist/history/claude.js +8 -2
- package/server/dist/http/routes/agents.js +1 -1
- package/server/dist/http/validate.js +2 -0
- package/server/dist/instructions.js +4 -0
- package/server/dist/models.js +2 -0
- package/server/dist/objects/index.js +4 -6
- package/server/dist/objects/runner.js +20 -0
- package/server/dist/objects/supervisor.js +2 -2
- package/server/dist/providers/claude.js +13 -1
- package/server/dist/providers/opencode-server.js +59 -0
- package/server/dist/providers/opencode.js +4 -1
- package/server/dist/stats.js +15 -3
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/app-build-manifest.json +80 -71
- package/web/.next/app-path-routes-manifest.json +6 -5
- package/web/.next/build-manifest.json +2 -2
- package/web/.next/prerender-manifest.json +73 -49
- package/web/.next/routes-manifest.json +6 -0
- package/web/.next/server/app/_not-found/page.js +1 -1
- package/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/_not-found.html +1 -1
- package/web/.next/server/app/_not-found.rsc +3 -3
- package/web/.next/server/app/agents/[id]/page.js +4 -4
- package/web/.next/server/app/agents/[id]/page.js.nft.json +1 -1
- package/web/.next/server/app/agents/[id]/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/agents/page.js +1 -1
- package/web/.next/server/app/agents/page.js.nft.json +1 -1
- package/web/.next/server/app/agents/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/agents.html +1 -1
- package/web/.next/server/app/agents.rsc +4 -4
- package/web/.next/server/app/chat/page.js +2 -0
- package/web/.next/server/app/chat/page.js.nft.json +1 -0
- package/web/.next/server/app/chat/page_client-reference-manifest.js +1 -0
- package/web/.next/server/app/chat.html +1 -0
- package/web/.next/server/app/chat.meta +15 -0
- package/web/.next/server/app/chat.rsc +25 -0
- package/web/.next/server/app/connections/page.js +1 -1
- package/web/.next/server/app/connections/page.js.nft.json +1 -1
- package/web/.next/server/app/connections/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/connections.html +1 -1
- package/web/.next/server/app/connections.rsc +4 -4
- package/web/.next/server/app/index.html +1 -1
- package/web/.next/server/app/index.rsc +4 -4
- package/web/.next/server/app/page.js +1 -1
- package/web/.next/server/app/page.js.nft.json +1 -1
- package/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/relations/page.js +2 -2
- package/web/.next/server/app/relations/page.js.nft.json +1 -1
- package/web/.next/server/app/relations/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/relations.html +1 -1
- package/web/.next/server/app/relations.rsc +4 -4
- package/web/.next/server/app/resources/[id]/page.js +2 -2
- package/web/.next/server/app/resources/[id]/page.js.nft.json +1 -1
- package/web/.next/server/app/resources/[id]/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/resources/page.js +2 -2
- package/web/.next/server/app/resources/page.js.nft.json +1 -1
- package/web/.next/server/app/resources/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/resources.html +1 -1
- package/web/.next/server/app/resources.rsc +4 -4
- package/web/.next/server/app/schedules/page.js +1 -1
- package/web/.next/server/app/schedules/page.js.nft.json +1 -1
- package/web/.next/server/app/schedules/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/schedules.html +1 -1
- package/web/.next/server/app/schedules.rsc +4 -4
- package/web/.next/server/app/sessions/page.js +2 -2
- package/web/.next/server/app/sessions/page.js.nft.json +1 -1
- package/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/sessions.html +1 -1
- package/web/.next/server/app/sessions.rsc +4 -4
- package/web/.next/server/app/skills/page.js +1 -1
- package/web/.next/server/app/skills/page.js.nft.json +1 -1
- package/web/.next/server/app/skills/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/skills.html +1 -1
- package/web/.next/server/app/skills.rsc +4 -4
- package/web/.next/server/app/types/page.js +1 -1
- package/web/.next/server/app/types/page.js.nft.json +1 -1
- package/web/.next/server/app/types/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/types.html +1 -1
- package/web/.next/server/app/types.rsc +4 -4
- package/web/.next/server/app-paths-manifest.json +6 -5
- package/web/.next/server/chunks/143.js +1 -1
- package/web/.next/server/chunks/148.js +1 -0
- package/web/.next/server/chunks/213.js +6 -0
- package/web/.next/server/chunks/274.js +1 -1
- package/web/.next/server/chunks/368.js +1 -0
- package/web/.next/server/chunks/403.js +1 -0
- package/web/.next/server/chunks/{790.js → 435.js} +2 -2
- package/web/.next/server/chunks/505.js +1 -6
- package/web/.next/server/chunks/612.js +1 -1
- package/web/.next/server/chunks/716.js +2 -2
- package/web/.next/server/pages/404.html +1 -1
- package/web/.next/server/pages/500.html +1 -1
- package/web/.next/server/server-reference-manifest.json +1 -1
- package/web/.next/static/YT6HM6qmegMJaBMX5QvuP/_buildManifest.js +1 -0
- package/web/.next/static/chunks/281-113274e705940a55.js +1 -0
- package/web/.next/static/chunks/350-0fe82e622e7a670f.js +1 -0
- package/web/.next/static/chunks/391-18768499e741112e.js +1 -0
- package/web/.next/static/chunks/517-373a80c8ba432bdc.js +1 -0
- package/web/.next/static/chunks/587-8e4d2be01ba7d48f.js +1 -0
- package/web/.next/static/chunks/606-c9a19918b02a98d0.js +1 -0
- package/web/.next/static/chunks/654-92a63b3c4b412666.js +1 -0
- package/web/.next/static/chunks/656-f537149fe28f79aa.js +1 -0
- package/web/.next/static/chunks/705-f81face97b1bb1af.js +1 -0
- package/web/.next/static/chunks/app/agents/[id]/page-6ed6edb3e74b0ebe.js +1 -0
- package/web/.next/static/chunks/app/agents/{page-f4c4c80f955f3967.js → page-9bb8cba69776c719.js} +1 -1
- package/web/.next/static/chunks/app/chat/page-5261b12b7d50f5d1.js +1 -0
- package/web/.next/static/chunks/app/connections/{page-0936beaf02d863c6.js → page-eb11cfb1ffeab3ca.js} +1 -1
- package/web/.next/static/chunks/app/layout-fad968a635f9a4b3.js +1 -0
- package/web/.next/static/chunks/app/page-99e0351e6ccaa384.js +1 -0
- package/web/.next/static/chunks/app/relations/{page-fba25393c8688714.js → page-a44fd515a3b0525e.js} +1 -1
- package/web/.next/static/chunks/app/resources/[id]/{page-d110d89bb8b5fca7.js → page-fc494e85b01e8cb7.js} +1 -1
- package/web/.next/static/chunks/app/resources/{page-bbd0b450d3f3131d.js → page-61cccd8fdcc2f6c8.js} +1 -1
- package/web/.next/static/chunks/app/schedules/{page-a1124397fc760413.js → page-96976fff4ee23538.js} +1 -1
- package/web/.next/static/chunks/app/sessions/{page-0e322811b0cde724.js → page-a7534ebd90e982d6.js} +1 -1
- package/web/.next/static/chunks/app/skills/{page-22ea0ef89d9f7a13.js → page-bc1acd0663f36129.js} +1 -1
- package/web/.next/static/chunks/app/types/{page-18c4e1db2724d41d.js → page-ebd37e220f77c1f3.js} +1 -1
- package/web/.next/static/css/582723fcea1f9887.css +1 -0
- package/web/.next/trace +30 -29
- package/web/.next/types/app/chat/page.ts +84 -0
- package/web/.next/types/routes.d.ts +2 -1
- package/web/.next/types/validator.ts +9 -0
- package/web/.next/server/chunks/306.js +0 -1
- package/web/.next/server/chunks/555.js +0 -1
- package/web/.next/server/chunks/638.js +0 -1
- package/web/.next/server/chunks/802.js +0 -1
- package/web/.next/static/7BI__-ayGDTlOrSboD_bT/_buildManifest.js +0 -1
- package/web/.next/static/chunks/176-aeaf907a6ea5627d.js +0 -1
- package/web/.next/static/chunks/291-efae53ea5f7ee92d.js +0 -1
- package/web/.next/static/chunks/350-56599dc3bf06066d.js +0 -1
- package/web/.next/static/chunks/486-13e08542f3a1cbac.js +0 -1
- package/web/.next/static/chunks/491-6b4e6201f5110dff.js +0 -1
- package/web/.next/static/chunks/517-ccaf8cc5c733cbd8.js +0 -1
- package/web/.next/static/chunks/581-3ad5c4343dad3f50.js +0 -1
- package/web/.next/static/chunks/705-c7e75806f77b880c.js +0 -1
- package/web/.next/static/chunks/839-4aefe6e71be6e1ef.js +0 -1
- package/web/.next/static/chunks/844-fe55292e0701f436.js +0 -1
- package/web/.next/static/chunks/app/agents/[id]/page-3faf40800981a736.js +0 -1
- package/web/.next/static/chunks/app/layout-99620746d028dda8.js +0 -1
- package/web/.next/static/chunks/app/page-a7f292117b7d76bf.js +0 -1
- package/web/.next/static/css/b7a03c62468f4263.css +0 -1
- /package/web/.next/static/{7BI__-ayGDTlOrSboD_bT → YT6HM6qmegMJaBMX5QvuP}/_ssgManifest.js +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,37 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.4.0] - 2026-10-15
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Chat** item in the side rail, above Colony. It opens the last conversation you had open (or the first agent if that one no longer exists). With no agents yet, it shows an empty state with a button to create the first one.
|
|
14
|
+
- **Resources strip** above each chat, folded by default. Closed, it is one line that counts the resources running, stopped or failing (failures in red). Open, it shows a card per resource with start or stop, restart and a link to its own screen. It shows the agent's resources and those of its colony, with a switch to see all of them; the choice and whether it is open are remembered.
|
|
15
|
+
- The README now has screenshots of the colony, chat, resources, clusters, changes explorer and HTTP requests.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Servers: **Keeping it healthy** is split into **If it goes down** (restart policy and maximum restarts) and **Health check** (kind, target and restart when unhealthy). The check timing (every, wait, failures in a row) and the stop and log settings are folded, and the defaults are visible.
|
|
20
|
+
- Servers: a restart waits for the port to be free before starting again, so a stop command that does not really stop the service is reported instead of leaving a failed start behind.
|
|
21
|
+
- Servers: the first health check comes one interval after a start instead of right away.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Entering a chat no longer flashes the default layout first: saved choices (the agent list collapsed, the resources strip open) apply before the first paint, the side panel only animates when you toggle it, and the conversation fades in when its history arrives.
|
|
26
|
+
|
|
27
|
+
## [1.3.0] - 2026-10-14
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- Claude agents can drive your Chrome through the Claude in Chrome extension. Turn on **Browser (Chrome)** in the agent's settings (it is off by default and adds nothing to the prompt). The chat shows each browser action (open page, read, click, run script) with its own icon and label. Chrome and the extension must be open on the machine where hive-am runs.
|
|
32
|
+
- Kiro: agents get an instruction to fully detach long-running processes (`setsid nohup … > log 2>&1 < /dev/null &`) so the shell tool returns at once and the turn and chat keep going, instead of waiting for the command and everything it started to finish.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- Claude: after **/compact** the chat no longer shows the CLI's summary as if you had written it; a "Conversation compacted" divider with the size before and after appears instead.
|
|
37
|
+
- Claude: the context percentage drops after a compact, using the size the CLI reports, instead of keeping the old value until the next reply.
|
|
38
|
+
- Context gauge: it uses each model's real window instead of a fixed 200k, so a 1M model no longer shows 100% at 200k tokens. The window is learned from the CLIs and kept in `context-windows.json`: Claude reports it in every turn, Kiro lists it with its models and OpenCode has it in its model catalog (read once an hour with a short-lived server when a model is unknown). When no window is known the chat shows tokens instead of a made-up percentage. The gauge also counts the last reply, ignores synthetic entries (errors) that used to reset it, and its tooltip shows tokens used and window.
|
|
39
|
+
|
|
9
40
|
## [1.2.2] - 2026-10-13
|
|
10
41
|
|
|
11
42
|
### Fixed
|
|
@@ -155,7 +186,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
|
155
186
|
- Complete documentation in English and Spanish.
|
|
156
187
|
- npm package with the `hive-am` command.
|
|
157
188
|
|
|
158
|
-
[Unreleased]: https://github.com/hughcbscrt/hive-am/compare/v1.
|
|
189
|
+
[Unreleased]: https://github.com/hughcbscrt/hive-am/compare/v1.4.0...HEAD
|
|
190
|
+
[1.4.0]: https://github.com/hughcbscrt/hive-am/compare/v1.3.0...v1.4.0
|
|
191
|
+
[1.3.0]: https://github.com/hughcbscrt/hive-am/compare/v1.2.2...v1.3.0
|
|
159
192
|
[1.2.2]: https://github.com/hughcbscrt/hive-am/compare/v1.2.1...v1.2.2
|
|
160
193
|
[1.2.1]: https://github.com/hughcbscrt/hive-am/compare/v1.2.0...v1.2.1
|
|
161
194
|
[1.2.0]: https://github.com/hughcbscrt/hive-am/compare/v1.1.0...v1.2.0
|
package/README.md
CHANGED
|
@@ -44,6 +44,25 @@ have installed; it does not replace them and it never copies your conversations
|
|
|
44
44
|
- **Terminals.** A bottom panel (Ctrl+`) with real shells and object logs, reachable only from this machine.
|
|
45
45
|
- **Local and private.** The server binds to `127.0.0.1`. Data lives in `~/.hive-am/`.
|
|
46
46
|
|
|
47
|
+
## Screenshots
|
|
48
|
+
|
|
49
|
+
<table>
|
|
50
|
+
<tr>
|
|
51
|
+
<td width="50%"><img src="docs/manual/en/img/colonia-mapa.jpg" alt="The colony map: colonies, agents and resources as hexagons" /><br /><sub><b>The colony.</b> Agents and resources as hexagons, grouped by colony.</sub></td>
|
|
52
|
+
<td width="50%"><img src="docs/manual/en/img/chat-conversacion.jpg" alt="A conversation with an orchestrator" /><br /><sub><b>Chat.</b> The real session of the CLI, with tokens, cost and tool calls.</sub></td>
|
|
53
|
+
</tr>
|
|
54
|
+
<tr>
|
|
55
|
+
<td width="50%"><img src="docs/manual/en/img/recurso-servidor-logs.jpg" alt="A server resource with its live logs" /><br /><sub><b>Resources.</b> Servers, containers and databases with live logs.</sub></td>
|
|
56
|
+
<td width="50%"><img src="docs/manual/en/img/recurso-cluster.jpg" alt="A cluster that starts several resources together" /><br /><sub><b>Clusters.</b> Start and stop a group of resources in order.</sub></td>
|
|
57
|
+
</tr>
|
|
58
|
+
<tr>
|
|
59
|
+
<td width="50%"><img src="docs/manual/en/img/cambios-diff.jpg" alt="The changes explorer showing a diff" /><br /><sub><b>Changes.</b> Git tree, diff, history and commit per agent.</sub></td>
|
|
60
|
+
<td width="50%"><img src="docs/manual/en/img/http-respuesta.jpg" alt="An HTTP request and its response" /><br /><sub><b>HTTP requests.</b> Run <code>.http</code> files and read the response.</sub></td>
|
|
61
|
+
</tr>
|
|
62
|
+
</table>
|
|
63
|
+
|
|
64
|
+
More in the [user manual](docs/manual/en/README.md).
|
|
65
|
+
|
|
47
66
|
## Providers
|
|
48
67
|
|
|
49
68
|
| Provider | Run a turn | Transcript source |
|
package/package.json
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { DATA_DIR } from './db.js';
|
|
4
|
+
/**
|
|
5
|
+
* Size of each model's context window, learned from what the CLIs report: Claude says it in every result, Kiro lists it with its
|
|
6
|
+
* models and OpenCode's server has a catalog. A saved conversation does not keep it, so what was learned is stored on disk
|
|
7
|
+
* (`context-windows.json`) and used for any conversation of that model, also from before it was learned.
|
|
8
|
+
*/
|
|
9
|
+
const FILE = join(DATA_DIR, 'context-windows.json');
|
|
10
|
+
let known = null;
|
|
11
|
+
const key = (provider, model) => `${provider}:${model}`;
|
|
12
|
+
function load() {
|
|
13
|
+
if (known)
|
|
14
|
+
return known;
|
|
15
|
+
try {
|
|
16
|
+
known = existsSync(FILE) ? JSON.parse(readFileSync(FILE, 'utf8')) : {};
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
known = {};
|
|
20
|
+
}
|
|
21
|
+
return known;
|
|
22
|
+
}
|
|
23
|
+
/** Remembers a model's window. Writes the file only when something changed. */
|
|
24
|
+
export function learnWindows(provider, windows) {
|
|
25
|
+
const all = load();
|
|
26
|
+
let changed = false;
|
|
27
|
+
for (const [model, tokens] of Object.entries(windows)) {
|
|
28
|
+
if (!model || !Number.isFinite(tokens) || tokens <= 0)
|
|
29
|
+
continue;
|
|
30
|
+
const k = key(provider, model);
|
|
31
|
+
if (all[k] !== tokens) {
|
|
32
|
+
all[k] = tokens;
|
|
33
|
+
changed = true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (changed) {
|
|
37
|
+
try {
|
|
38
|
+
writeFileSync(FILE, JSON.stringify(all, null, 1));
|
|
39
|
+
}
|
|
40
|
+
catch { /* best effort: it is learned again next time */ }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The window of a model in tokens, or null when it is not known (the chat then shows tokens instead of a made-up percentage).
|
|
45
|
+
* For a Claude model that was never seen on this machine it is derived from the id: Claude 5 and newer have 1M, older ones 200k,
|
|
46
|
+
* and a `[1m]` suffix is the 1M variant.
|
|
47
|
+
*/
|
|
48
|
+
export function contextWindow(provider, model) {
|
|
49
|
+
if (!provider || !model || model.startsWith('<'))
|
|
50
|
+
return null;
|
|
51
|
+
const learned = load()[key(provider, model)];
|
|
52
|
+
if (learned)
|
|
53
|
+
return learned;
|
|
54
|
+
if (provider !== 'claude')
|
|
55
|
+
return null;
|
|
56
|
+
if (/\[1m\]/i.test(model))
|
|
57
|
+
return 1_000_000;
|
|
58
|
+
const major = /^claude-(?:opus|sonnet|haiku)-(\d+)/.exec(model)?.[1];
|
|
59
|
+
if (major)
|
|
60
|
+
return Number(major) >= 5 ? 1_000_000 : 200_000;
|
|
61
|
+
return model.startsWith('claude-') ? 200_000 : null;
|
|
62
|
+
}
|
|
@@ -182,6 +182,13 @@ CREATE TABLE IF NOT EXISTS object_agents (
|
|
|
182
182
|
db.exec("UPDATE skills SET name='Colony resources' WHERE id='default-objects' AND name='Colony objects' AND NOT EXISTS (SELECT 1 FROM skills WHERE lower(name)='colony resources')");
|
|
183
183
|
},
|
|
184
184
|
},
|
|
185
|
+
{
|
|
186
|
+
version: 3, name: 'agent-browser',
|
|
187
|
+
up(db) {
|
|
188
|
+
// Whether a Claude agent may drive the person's Chrome (`--chrome`). Off for every agent that already exists.
|
|
189
|
+
addColumn(db, 'agents', 'browser INTEGER NOT NULL DEFAULT 0');
|
|
190
|
+
},
|
|
191
|
+
},
|
|
185
192
|
];
|
|
186
193
|
export const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
|
|
187
194
|
/** Brings a database up to the current schema. Returns the versions it applied (empty when it was already current). */
|
package/server/dist/db.js
CHANGED
|
@@ -184,7 +184,7 @@ function agentOf(r) {
|
|
|
184
184
|
skill_loads: { ...(follows('skills') ? col.skill_loads : {}), ...loadsOf('agent', r.id) },
|
|
185
185
|
inherited,
|
|
186
186
|
};
|
|
187
|
-
return { ...r, skill_ids, skill_loads: loadsOf('agent', r.id), worker_ids, overrides, effective };
|
|
187
|
+
return { ...r, browser: !!r.browser, skill_ids, skill_loads: loadsOf('agent', r.id), worker_ids, overrides, effective };
|
|
188
188
|
}
|
|
189
189
|
/** The agent as it actually runs: colony-inherited values already folded into cwd/permission/prompt/skills. */
|
|
190
190
|
export function resolved(a) {
|
|
@@ -197,7 +197,7 @@ export const agents = {
|
|
|
197
197
|
/** The agent, its skills and its workers are written together: either all of it exists afterwards or none. */
|
|
198
198
|
create: db.transaction((p) => {
|
|
199
199
|
const id = uid(), t = now();
|
|
200
|
-
q('INSERT INTO agents (id,name,description,role,type_id,provider,model,system_prompt,permission,cwd,session_id,status,created_at,updated_at,colony_id,overrides) VALUES (?,?,?,?,?,?,?,?,?,?,NULL
|
|
200
|
+
q('INSERT INTO agents (id,name,description,role,type_id,provider,model,system_prompt,permission,cwd,session_id,status,created_at,updated_at,colony_id,overrides,browser) VALUES (?,?,?,?,?,?,?,?,?,?,NULL,?,?,?,?,?,?)').run(id, p.name, p.description ?? '', p.role, p.type_id ?? null, p.provider, p.model ?? '', p.system_prompt ?? '', (p.permission ?? 'acceptEdits'), p.cwd ?? '', 'idle', t, t, p.colony_id ?? null, JSON.stringify(p.overrides ?? []), p.browser && p.provider === 'claude' ? 1 : 0);
|
|
201
201
|
agents.setSkills(id, p.skill_ids ?? [], p.skill_loads);
|
|
202
202
|
agents.setWorkers(id, p.worker_ids ?? []);
|
|
203
203
|
return agents.get(id);
|
|
@@ -207,8 +207,8 @@ export const agents = {
|
|
|
207
207
|
if (!cur)
|
|
208
208
|
return;
|
|
209
209
|
const n = { ...cur, ...p };
|
|
210
|
-
q('UPDATE agents SET name=?, description=?, role=?, type_id=?, provider=?, model=?, system_prompt=?, permission=?, cwd=?, colony_id=?, overrides=?, updated_at=? WHERE id=?')
|
|
211
|
-
.run(n.name, n.description, n.role, n.type_id, n.provider, n.model, n.system_prompt, n.permission, n.cwd, n.colony_id ?? null, JSON.stringify(n.overrides ?? []), now(), id);
|
|
210
|
+
q('UPDATE agents SET name=?, description=?, role=?, type_id=?, provider=?, model=?, system_prompt=?, permission=?, cwd=?, colony_id=?, overrides=?, browser=?, updated_at=? WHERE id=?')
|
|
211
|
+
.run(n.name, n.description, n.role, n.type_id, n.provider, n.model, n.system_prompt, n.permission, n.cwd, n.colony_id ?? null, JSON.stringify(n.overrides ?? []), n.browser && n.provider === 'claude' ? 1 : 0, now(), id);
|
|
212
212
|
if (p.skill_ids || p.skill_loads)
|
|
213
213
|
agents.setSkills(id, p.skill_ids ?? cur.skill_ids, p.skill_loads);
|
|
214
214
|
if (p.worker_ids)
|
|
@@ -61,7 +61,13 @@ function parseClaude(file) {
|
|
|
61
61
|
const toolBlocks = new Map();
|
|
62
62
|
const toolStart = new Map();
|
|
63
63
|
for (const e of jsonLines(file)) {
|
|
64
|
-
|
|
64
|
+
// `/compact` leaves a boundary (with the size before and after) and the summary as a user entry: the summary is the CLI's, not the person's.
|
|
65
|
+
if (e.type === 'system' && e.subtype === 'compact_boundary') {
|
|
66
|
+
const c = e.compactMetadata ?? {};
|
|
67
|
+
out.push({ id: e.uuid, role: 'assistant', ts: e.timestamp ? Date.parse(e.timestamp) : null, blocks: [], compact: { pre: c.preTokens ?? 0, post: c.postTokens ?? 0 } });
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (e.isSidechain || e.isMeta || e.isCompactSummary || (e.type !== 'user' && e.type !== 'assistant'))
|
|
65
71
|
continue;
|
|
66
72
|
const m = e.message;
|
|
67
73
|
if (!m)
|
|
@@ -133,7 +139,7 @@ function parseClaude(file) {
|
|
|
133
139
|
}
|
|
134
140
|
}
|
|
135
141
|
}
|
|
136
|
-
return out.filter((m) => m.blocks.length);
|
|
142
|
+
return out.filter((m) => m.blocks.length || m.compact);
|
|
137
143
|
}
|
|
138
144
|
export function claudeMeta(file) {
|
|
139
145
|
const st = statSync(file);
|
|
@@ -104,7 +104,7 @@ route('GET', '/api/agents/:id/history', ({ params, url }) => {
|
|
|
104
104
|
});
|
|
105
105
|
route('GET', '/api/agents/:id/stats', ({ params, url }) => {
|
|
106
106
|
const a = agentOrFail(params[0]);
|
|
107
|
-
return sessionStats(readHistory(resolved(a), sessionToRead(a, url.searchParams.get('session'))));
|
|
107
|
+
return sessionStats(readHistory(resolved(a), sessionToRead(a, url.searchParams.get('session'))), a.provider);
|
|
108
108
|
});
|
|
109
109
|
route('GET', '/api/agents/:id/live', ({ params }) => liveTurn(params[0]));
|
|
110
110
|
route('GET', '/api/agents/:id/sessions', ({ params }) => agents.sessions(params[0]));
|
|
@@ -27,6 +27,8 @@ export function validateAgentInput(p, partial = false) {
|
|
|
27
27
|
if (p.role !== undefined && !['orchestrator', 'worker'].includes(p.role))
|
|
28
28
|
throw bad('Role must be orchestrator or worker');
|
|
29
29
|
checkFolder(p.cwd);
|
|
30
|
+
if (p.browser !== undefined && typeof p.browser !== 'boolean')
|
|
31
|
+
throw bad('browser must be true or false');
|
|
30
32
|
if (p.colony_id && (typeof p.colony_id !== 'string' || !colonies.get(p.colony_id)))
|
|
31
33
|
throw bad('That colony no longer exists');
|
|
32
34
|
return p;
|
|
@@ -54,6 +54,10 @@ export function composeInstructions(a, delegated = false) {
|
|
|
54
54
|
if (a.provider === 'opencode' && mcpCaps(a).length > 0)
|
|
55
55
|
parts.push(`## Calling the hive tools
|
|
56
56
|
The hive tools (\`tools.hive.*\`) are called from the \`execute\` tool, with \`await\`, using exactly the names given in these instructions. Do not \`search\` for them and do not probe with \`shell\` first: call them directly, e.g. \`execute\` with \`return await tools.hive.channel_reply({ text: "..." });\`. At the start of a turn the hive tools are still connecting, so your very first call can fail with \`Unknown tool 'hive.…'. Use search to find available tools.\` That is expected: ignore the hint, do not \`search\` and do not change tools; make the same call again right away with the same arguments and it will work. Use \`shell\` only for real shell work (commands, files), never to run JavaScript or as a placeholder. For a simple question, answer it in a single call.`);
|
|
57
|
+
// Kiro's shell tool returns only when the command's output pipes close, so a plain `cmd &` keeps the whole turn (and the chat) waiting until it ends.
|
|
58
|
+
if (a.provider === 'kiro')
|
|
59
|
+
parts.push(`## Background commands
|
|
60
|
+
Your shell tool waits until the command and everything it started has closed its output. To leave something running (a dev server, a watcher, a long build, a tunnel), detach it completely so the tool returns at once and the conversation can go on: \`setsid nohup <command> > <log file> 2>&1 < /dev/null & echo $!\`. All three redirections are required; a bare \`<command> &\` still blocks. Then read the log file when you need its output, and never wait for the process to end.`);
|
|
57
61
|
if (a.system_prompt.trim())
|
|
58
62
|
parts.push(a.system_prompt.trim());
|
|
59
63
|
const lazy = lazySkills(a);
|
package/server/dist/models.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
|
+
import { learnWindows } from './context.js';
|
|
3
4
|
const run = promisify(execFile);
|
|
4
5
|
const cache = new Map();
|
|
5
6
|
const CLAUDE = [
|
|
@@ -21,6 +22,7 @@ async function discover(p) {
|
|
|
21
22
|
const { stdout } = await run('kiro-cli', ['chat', '--list-models', '--format', 'json'], { timeout: 20000, maxBuffer: 4e6 });
|
|
22
23
|
const j = JSON.parse(stdout);
|
|
23
24
|
const list = Array.isArray(j) ? j : j.models ?? [];
|
|
25
|
+
learnWindows('kiro', Object.fromEntries(list.map((m) => [m.model_id ?? m.id ?? m.name, Number(m.context_window_tokens)])));
|
|
24
26
|
return list.map((m) => ({ id: m.model_id ?? m.id ?? m.name, label: m.model_name ?? m.name ?? m.model_id ?? m.id })).filter((m) => m.id);
|
|
25
27
|
}
|
|
26
28
|
catch {
|
|
@@ -10,7 +10,7 @@ import { dbColumns, dbQuery, dbSchema, dbState, dbTest, forgetDatabase } from '.
|
|
|
10
10
|
import { addSaved, clearQueries, forgetQueries, listQueries, listSaved, recordQuery, removeSaved, updateSaved } from './db-history.js';
|
|
11
11
|
import { exportCollection, parseHttpFile, createHttpFile, exportHttp, describeHttp, dropSession, inspectHttp, readHttpFile, runHttp, scanHttp, writeHttpFile, sessionClear, sessionOf, sessionSet, sessionView } from './http.js';
|
|
12
12
|
import { ObjectError, parseDatabase } from './model.js';
|
|
13
|
-
import { forgetServer, serverLogs, serverState, startServer, stopServer } from './runner.js';
|
|
13
|
+
import { forgetServer, serverLogs, restartServer, serverState, startServer, stopServer } from './runner.js';
|
|
14
14
|
import { forgetSupervision, resetSupervision, supervise } from './supervisor.js';
|
|
15
15
|
import { maskConfig, objectsStore } from './store.js';
|
|
16
16
|
export { ObjectError } from './model.js';
|
|
@@ -59,7 +59,7 @@ function clusterState(o) {
|
|
|
59
59
|
async function serverStateSupervised(o) {
|
|
60
60
|
const cfg = o.config;
|
|
61
61
|
const base = await serverState(o.id, cfg);
|
|
62
|
-
return supervise(o, cfg, base, { start: () => startServer(o.id, cfg), restart:
|
|
62
|
+
return supervise(o, cfg, base, { start: () => startServer(o.id, cfg), restart: () => restartServer(o.id, cfg) });
|
|
63
63
|
}
|
|
64
64
|
export async function stateOf(o) {
|
|
65
65
|
const s = o.kind === 'server' ? await serverStateSupervised(o)
|
|
@@ -113,10 +113,8 @@ async function act(o, action) {
|
|
|
113
113
|
await startServer(o.id, c);
|
|
114
114
|
else if (action === 'stop')
|
|
115
115
|
await stopServer(o.id, c);
|
|
116
|
-
else
|
|
117
|
-
await
|
|
118
|
-
await startServer(o.id, c);
|
|
119
|
-
}
|
|
116
|
+
else
|
|
117
|
+
await restartServer(o.id, c);
|
|
120
118
|
}
|
|
121
119
|
else {
|
|
122
120
|
const c = o.config;
|
|
@@ -139,6 +139,26 @@ export async function stopServer(id, cfg) {
|
|
|
139
139
|
await sleep(100);
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Stop, then start again. With a port, the old listener must be gone before the new one starts: otherwise the check on the port would
|
|
144
|
+
* report "running" for the old process (a stop command that does not really stop the service) while the new one failed to bind.
|
|
145
|
+
*/
|
|
146
|
+
export async function restartServer(id, cfg) {
|
|
147
|
+
await stopServer(id, cfg);
|
|
148
|
+
if (cfg.port) {
|
|
149
|
+
const grace = cfg.stopTimeoutSec ? cfg.stopTimeoutSec * 1000 : STOP_GRACE_MS;
|
|
150
|
+
let waited = 0;
|
|
151
|
+
while (await listening(cfg.port) && waited < grace) {
|
|
152
|
+
await sleep(300);
|
|
153
|
+
waited += 300;
|
|
154
|
+
}
|
|
155
|
+
if (await listening(cfg.port)) {
|
|
156
|
+
note(id, `port ${cfg.port} is still in use after stopping: not starting it again. Check that the stop command really stops the service`);
|
|
157
|
+
throw new ObjectError(`Port ${cfg.port} is still in use after stopping, so it was not started again. Check that the stop command really stops the service.`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
await startServer(id, cfg);
|
|
161
|
+
}
|
|
142
162
|
/** Lines the object printed. `after` is a byte offset from an earlier call; without it the last `tail` lines come back. */
|
|
143
163
|
export function serverLogs(id, o) {
|
|
144
164
|
const { log } = files(id);
|
|
@@ -54,8 +54,8 @@ export async function supervise(o, cfg, state, act) {
|
|
|
54
54
|
if (S.startedAt !== saved?.startedAt) {
|
|
55
55
|
S.startedAt = saved?.startedAt ?? 0;
|
|
56
56
|
S.fails = 0;
|
|
57
|
-
S.lastCheck =
|
|
58
|
-
}
|
|
57
|
+
S.lastCheck = now;
|
|
58
|
+
} // a new start: the first check comes one interval later, not right away
|
|
59
59
|
if (now - S.lastCheck >= (cfg.health.intervalSec ?? 15) * 1000) {
|
|
60
60
|
S.lastCheck = now;
|
|
61
61
|
const r = await check(cfg, cfg.health);
|
|
@@ -6,6 +6,7 @@ import { dispatchMcpConfig } from '../mcp-config.js';
|
|
|
6
6
|
import { existsSync } from 'node:fs';
|
|
7
7
|
import { inboxDir } from '../connections/files.js';
|
|
8
8
|
import { withInstructions } from './preamble.js';
|
|
9
|
+
import { learnWindows } from '../context.js';
|
|
9
10
|
/** What editing a project must not touch: the repository's own data and private keys (.env stays editable, like in OpenCode). */
|
|
10
11
|
export const CLAUDE_EDIT_DENY = ['Edit(**/.git/**)', 'Edit(**/*.pem)', 'Edit(**/*.key)'];
|
|
11
12
|
/** Claude Code: `claude -p` with stream-json; the prompt goes through stdin, the session is resumed by id. */
|
|
@@ -28,13 +29,22 @@ export async function* runClaude(o) {
|
|
|
28
29
|
if (o.instructions)
|
|
29
30
|
args.push('--append-system-prompt', o.instructions);
|
|
30
31
|
const prompt = withInstructions(o.prompt, o.instructions, !!agent.session_id, o.refreshInstructions);
|
|
32
|
+
// Agents with the Browser switch on drive the person's Chrome through its extension; `-p` has nobody to approve each call, so the tools are allowed up front.
|
|
33
|
+
const allowed = [];
|
|
34
|
+
if (agent.browser && agent.permission !== 'plan') {
|
|
35
|
+
args.push('--chrome');
|
|
36
|
+
allowed.push('mcp__claude-in-chrome');
|
|
37
|
+
}
|
|
31
38
|
if (o.mcpCaps.length) {
|
|
32
39
|
const dir = join(DATA_DIR, 'mcp');
|
|
33
40
|
mkdirSync(dir, { recursive: true });
|
|
34
41
|
const file = join(dir, `${agent.id}.json`);
|
|
35
42
|
writeFileSync(file, JSON.stringify(dispatchMcpConfig(agent.id, o.mcpCaps)));
|
|
36
|
-
args.push('--mcp-config', file
|
|
43
|
+
args.push('--mcp-config', file);
|
|
44
|
+
allowed.push('mcp__hive');
|
|
37
45
|
}
|
|
46
|
+
if (allowed.length)
|
|
47
|
+
args.push('--allowedTools', ...allowed);
|
|
38
48
|
const toolNames = new Map();
|
|
39
49
|
let streamedText = false;
|
|
40
50
|
let sawSession = false;
|
|
@@ -74,6 +84,8 @@ export async function* runClaude(o) {
|
|
|
74
84
|
}
|
|
75
85
|
}
|
|
76
86
|
else if (ev.type === 'result') {
|
|
87
|
+
// The CLI says how big each model's window is: keep it, a saved conversation does not.
|
|
88
|
+
learnWindows('claude', Object.fromEntries(Object.entries(ev.modelUsage ?? {}).map(([m, x]) => [m, Number(x?.contextWindow)])));
|
|
77
89
|
const u = ev.usage;
|
|
78
90
|
if (u)
|
|
79
91
|
yield {
|
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { DATA_DIR } from '../db.js';
|
|
5
5
|
import { createHash, randomBytes } from 'node:crypto';
|
|
6
|
+
import { learnWindows } from '../context.js';
|
|
6
7
|
/**
|
|
7
8
|
* One long-lived `opencode serve` per agent. A private server per turn (`opencode run --standalone`) boots from zero every time and its
|
|
8
9
|
* hive MCP server connects only after the model has already taken its first step, so the first `tools.hive.*` call always fails
|
|
@@ -57,6 +58,64 @@ function reapOrphans() {
|
|
|
57
58
|
}
|
|
58
59
|
reapOrphans();
|
|
59
60
|
const auth = (password) => ({ authorization: `Basic ${Buffer.from(`opencode:${password}`).toString('base64')}` });
|
|
61
|
+
let windowsAt = 0;
|
|
62
|
+
/**
|
|
63
|
+
* Reads the context window of every model from OpenCode's catalog, using this turn's server, any running one or a short-lived one
|
|
64
|
+
* (about 4 s and 150 MB, once). At most once an hour, and a failure waits ten minutes: OpenCode 1.x has no catalog endpoint at all.
|
|
65
|
+
*/
|
|
66
|
+
export async function learnOpencodeWindows(cwd, access) {
|
|
67
|
+
if (Date.now() - windowsAt < 60 * 60_000)
|
|
68
|
+
return;
|
|
69
|
+
windowsAt = Date.now();
|
|
70
|
+
let proc;
|
|
71
|
+
try {
|
|
72
|
+
let url = access?.url, password = access?.password;
|
|
73
|
+
if (!url) {
|
|
74
|
+
const live = [...servers.values()].find((x) => x.url && x.proc.exitCode === null);
|
|
75
|
+
if (live) {
|
|
76
|
+
url = live.url;
|
|
77
|
+
password = live.password;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (!url) {
|
|
81
|
+
password = randomBytes(18).toString('base64url');
|
|
82
|
+
const child = proc = spawn('opencode', ['serve', '--hostname', '127.0.0.1', '--port', '0'], { cwd, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, PWD: cwd, OPENCODE_SERVER_PASSWORD: password } });
|
|
83
|
+
url = await new Promise((resolve, reject) => {
|
|
84
|
+
let out = '';
|
|
85
|
+
const timer = setTimeout(() => reject(new Error('opencode serve did not start in time')), START_TIMEOUT_MS);
|
|
86
|
+
const onData = (d) => { out = (out + d).slice(-2000); const m = /listening on (http:\/\/\S+)/i.exec(out); if (m) {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
resolve(m[1].replace(/\/$/, ''));
|
|
89
|
+
} };
|
|
90
|
+
child.stdout.on('data', onData);
|
|
91
|
+
child.stderr.on('data', onData);
|
|
92
|
+
child.on('error', (e) => { clearTimeout(timer); reject(e); });
|
|
93
|
+
child.on('exit', (c) => { clearTimeout(timer); reject(new Error(`opencode serve exited with code ${c}`)); });
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
// Right after it starts the catalog is empty for a moment: ask again until it has models.
|
|
97
|
+
const until = Date.now() + 10_000;
|
|
98
|
+
let list = [];
|
|
99
|
+
while (!list.length && Date.now() < until) {
|
|
100
|
+
const r = await fetch(`${url}/api/model`, { headers: auth(password), signal: AbortSignal.timeout(5000) });
|
|
101
|
+
if (!r.ok)
|
|
102
|
+
return; // no catalog in this OpenCode: leave it for an hour
|
|
103
|
+
list = (await r.json())?.data ?? [];
|
|
104
|
+
if (!list.length)
|
|
105
|
+
await new Promise((res) => setTimeout(res, 400));
|
|
106
|
+
}
|
|
107
|
+
if (!list.length)
|
|
108
|
+
throw new Error('the model catalog stayed empty');
|
|
109
|
+
learnWindows('opencode', Object.fromEntries(list.map((m) => [`${m.providerID}/${m.id}`, Number(m.limit?.context)])));
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
windowsAt = Date.now() - 50 * 60_000; /* try again in ten minutes */
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
if (proc && !proc.killed)
|
|
116
|
+
proc.kill('SIGTERM');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
60
119
|
/** Waits until every MCP server the config asks for has finished connecting (connected or failed): no more "Unknown tool" at the first step. */
|
|
61
120
|
async function mcpSettled(s, signal) {
|
|
62
121
|
const until = Date.now() + MCP_WAIT_MS;
|
|
@@ -6,7 +6,8 @@ import { spawnJson } from './spawn.js';
|
|
|
6
6
|
import { dispatchMcpConfigOpencode } from '../mcp-config.js';
|
|
7
7
|
import { opencodeSessionDir } from '../history/opencode.js';
|
|
8
8
|
import { withInstructions } from './preamble.js';
|
|
9
|
-
import {
|
|
9
|
+
import { contextWindow } from '../context.js';
|
|
10
|
+
import { learnOpencodeWindows, opencodeServer, stopOpencodeServer } from './opencode-server.js';
|
|
10
11
|
/**
|
|
11
12
|
* What OpenCode may do for each permission level. It has no such modes, so they are written as its own rules (a `deny` holds even with `--auto`):
|
|
12
13
|
* read-only changes nothing; editing changes files of the project (including .env, never .git or keys), runs no commands and stays in the agent's folder
|
|
@@ -84,6 +85,8 @@ export async function* runOpencode(o) {
|
|
|
84
85
|
console.warn(`[opencode] no persistent server for ${agent.name}, using a private one: ${e instanceof Error ? e.message : e}`);
|
|
85
86
|
}
|
|
86
87
|
}
|
|
88
|
+
if (!contextWindow('opencode', agent.model))
|
|
89
|
+
void learnOpencodeWindows(agent.cwd, access);
|
|
87
90
|
if (access) {
|
|
88
91
|
args.splice(1, 0, '--server', access.url);
|
|
89
92
|
env.OPENCODE_SERVER_PASSWORD = access.password;
|
package/server/dist/stats.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { addUsage, emptyUsage } from './pricing.js';
|
|
2
|
-
|
|
2
|
+
import { contextWindow } from './context.js';
|
|
3
|
+
export function sessionStats(msgs, provider) {
|
|
3
4
|
let usage = emptyUsage();
|
|
4
5
|
let cost = 0;
|
|
5
6
|
let any = false;
|
|
@@ -9,8 +10,13 @@ export function sessionStats(msgs) {
|
|
|
9
10
|
const timeline = [];
|
|
10
11
|
let cur = null;
|
|
11
12
|
let lastCtx = 0;
|
|
13
|
+
let ctxModel;
|
|
12
14
|
let toolCalls = 0, toolErrors = 0, toolTime = 0;
|
|
13
15
|
for (const m of msgs) {
|
|
16
|
+
if (m.compact) {
|
|
17
|
+
lastCtx = m.compact.post;
|
|
18
|
+
continue;
|
|
19
|
+
} // nothing is sent after a compact, so the context is what it left
|
|
14
20
|
if (m.role === 'user') {
|
|
15
21
|
const prompt = m.blocks.map((b) => (b.type === 'text' ? b.text : '')).join('').slice(0, 120);
|
|
16
22
|
cur = { ts: m.ts, prompt, tokens: 0, cost: null, durationMs: 0, tools: 0 };
|
|
@@ -20,7 +26,11 @@ export function sessionStats(msgs) {
|
|
|
20
26
|
const u = m.meta?.usage;
|
|
21
27
|
if (u) {
|
|
22
28
|
usage = addUsage(usage, u);
|
|
23
|
-
|
|
29
|
+
// A synthetic entry (an error, "no response requested") has no real request behind it: it must not reset the gauge to 0.
|
|
30
|
+
if (u.input + u.cacheRead + u.cacheWrite > 0 && !m.meta?.model?.startsWith('<')) {
|
|
31
|
+
lastCtx = u.input + u.cacheRead + u.cacheWrite + u.output;
|
|
32
|
+
ctxModel = m.meta?.model ?? ctxModel;
|
|
33
|
+
}
|
|
24
34
|
if (cur)
|
|
25
35
|
cur.tokens += u.input + u.output + u.cacheRead + u.cacheWrite;
|
|
26
36
|
}
|
|
@@ -56,11 +66,13 @@ export function sessionStats(msgs) {
|
|
|
56
66
|
tools.set(b.name, t);
|
|
57
67
|
}
|
|
58
68
|
}
|
|
69
|
+
const window = contextWindow(provider, ctxModel);
|
|
59
70
|
return {
|
|
60
71
|
usage, cost: any ? cost : null, costEstimated: est, turns: timeline.length, toolCalls, toolErrors, toolTimeMs: toolTime,
|
|
61
72
|
durationMs: timeline.reduce((n, t) => n + t.durationMs, 0),
|
|
62
73
|
tools: [...tools.values()].sort((a, b) => b.count - a.count),
|
|
63
74
|
models: [...models.values()].sort((a, b) => b.messages - a.messages),
|
|
64
|
-
lastContext: lastCtx,
|
|
75
|
+
lastContext: lastCtx, contextWindow: window,
|
|
76
|
+
contextPct: usage.contextPct ?? (window && lastCtx ? Math.min(100, (lastCtx / window) * 100) : null), timeline,
|
|
65
77
|
};
|
|
66
78
|
}
|
package/web/.next/BUILD_ID
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
YT6HM6qmegMJaBMX5QvuP
|