@staix/agent-hub 0.12.15 → 0.12.17
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 +17 -0
- package/README.md +18 -5
- package/docs/agent-notes/adapters.md +1 -0
- package/docs/agent-notes/budget.md +5 -1
- package/docs/agent-notes/bus.md +2 -0
- package/docs/agent-notes/tasks.md +3 -1
- package/docs/agent-notes/tests.md +3 -1
- package/docs/operations.md +277 -11
- package/docs/quickstart.md +4 -4
- package/docs/security.md +26 -1
- package/docs/smoke.md +93 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +223 -0
- package/docs/verification/2026-10-09-agent-shell-t0.md +123 -0
- package/docs/verified.json +168 -0
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +18 -6
- package/src/adapters/acp.ts +2 -2
- package/src/adapters/claude-channel.ts +3 -2
- package/src/adapters/codex-appserver.ts +10 -2
- package/src/adapters/local-worker.ts +5 -5
- package/src/adapters/pi.ts +4 -14
- package/src/cli/console-state.ts +213 -0
- package/src/cli/console.ts +201 -0
- package/src/cli/facts-hook.ts +8 -1
- package/src/cli/identity-audit.ts +66 -0
- package/src/cli/identity.ts +54 -0
- package/src/cli/init.ts +40 -30
- package/src/cli/launch.ts +31 -1
- package/src/cli/main.ts +95 -39
- package/src/cli/preview.ts +80 -0
- package/src/cli/status-lines.ts +8 -2
- package/src/cli/statusline-tee.ts +14 -0
- package/src/cli/tail-render.ts +17 -0
- package/src/cli/upgrade-runtime.ts +1 -1
- package/src/hub/board.ts +9 -2
- package/src/hub/bus.ts +82 -10
- package/src/hub/child-process.ts +11 -0
- package/src/hub/conductor.ts +196 -0
- package/src/hub/context-window.ts +72 -0
- package/src/hub/control-client.ts +3 -3
- package/src/hub/daemon.ts +492 -77
- package/src/hub/envelope.ts +1 -1
- package/src/hub/events.ts +7 -1
- package/src/hub/hub-tools.ts +14 -1
- package/src/hub/report.ts +53 -4
- package/src/hub/supervision.ts +152 -0
- package/src/hub/task-sweep.ts +59 -0
- package/src/hub/tasks.ts +86 -15
- package/src/hub/usage.ts +37 -2
- package/src/models/relay.ts +6 -1
- package/src/pi/launch.ts +21 -0
- package/src/ui/index.html +6 -0
package/src/hub/usage.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { normalizeUsage, safeModelLabel, type NormalizedUsage } from "../omnirou
|
|
|
4
4
|
|
|
5
5
|
export interface UsageRecord {
|
|
6
6
|
id: string;
|
|
7
|
+
/** True only for a native assistant end_turn row, never a tool-use/compaction message. */
|
|
8
|
+
completedTurn?: boolean;
|
|
7
9
|
at?: string;
|
|
8
10
|
usage?: NormalizedUsage;
|
|
9
11
|
requestedModel?: string;
|
|
@@ -11,6 +13,39 @@ export interface UsageRecord {
|
|
|
11
13
|
provider?: string;
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
/** Claude-native counters have separate input/output/cache categories, often no explicit total. */
|
|
17
|
+
export function claudeReportedTokens(usage: NormalizedUsage | undefined): number | undefined {
|
|
18
|
+
if (!usage) return undefined;
|
|
19
|
+
if (usage.totalTokens !== undefined) return usage.totalTokens;
|
|
20
|
+
if (usage.inputTokens === undefined || usage.outputTokens === undefined) return undefined;
|
|
21
|
+
const total = usage.inputTokens + usage.outputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
|
|
22
|
+
return Number.isSafeInteger(total) && total >= 0 ? total : undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ClaudeCompletionWait = { record?: UsageRecord; reason: "verified" | "unavailable" | "baseline" | "before-start" | "superseded" };
|
|
26
|
+
/** After acknowledging Stop, observe its delayed transcript append within a bounded 1200 ms window. */
|
|
27
|
+
export async function waitForClaudeCompletion(read: () => UsageRecord | undefined, current: () => boolean, start: { at: number; baseline?: string }, timeoutMs = 1200): Promise<ClaudeCompletionWait> {
|
|
28
|
+
const budget = Number.isFinite(timeoutMs) ? Math.min(1200, Math.max(0, timeoutMs)) : 1200;
|
|
29
|
+
if (budget === 0) return { reason: "unavailable" };
|
|
30
|
+
const deadline = performance.now() + budget;
|
|
31
|
+
let reason: ClaudeCompletionWait["reason"] = "unavailable";
|
|
32
|
+
while (true) {
|
|
33
|
+
if (!current()) return { reason: "superseded" };
|
|
34
|
+
const record = read();
|
|
35
|
+
if (!current()) return { reason: "superseded" };
|
|
36
|
+
if (performance.now() > deadline) return { reason };
|
|
37
|
+
const at = typeof record?.at === "string" ? Date.parse(record.at) : NaN;
|
|
38
|
+
if (record?.completedTurn && Number.isFinite(at)) {
|
|
39
|
+
if (record.id === start.baseline) reason = "baseline";
|
|
40
|
+
else if (at < start.at) reason = "before-start";
|
|
41
|
+
else return { record, reason: "verified" };
|
|
42
|
+
} else reason = "unavailable";
|
|
43
|
+
const left = deadline - performance.now();
|
|
44
|
+
if (left <= 0) return { reason };
|
|
45
|
+
await Bun.sleep(Math.min(40, left));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
14
49
|
function opaqueId(sessionId: string, messageId: string): string {
|
|
15
50
|
return createHash("sha256").update(sessionId).update("\0").update(messageId).digest("hex");
|
|
16
51
|
}
|
|
@@ -30,7 +65,7 @@ export function readClaudeTranscriptUsage(sessionId: string, transcriptPath: str
|
|
|
30
65
|
// Signal unavailable coverage with a stable opaque id; a repeated poll dedupes in the report.
|
|
31
66
|
return [{ id: opaqueId(sessionId, "transcript-too-large") }];
|
|
32
67
|
}
|
|
33
|
-
const latest = new Map<string, { usage?: NormalizedUsage; at?: string; servedModel?: string }>();
|
|
68
|
+
const latest = new Map<string, { usage?: NormalizedUsage; at?: string; servedModel?: string; completedTurn?: boolean }>();
|
|
34
69
|
for (const line of readFileSync(transcriptPath, "utf8").split("\n")) {
|
|
35
70
|
if (!line.trim()) continue;
|
|
36
71
|
try {
|
|
@@ -43,7 +78,7 @@ export function readClaudeTranscriptUsage(sessionId: string, transcriptPath: str
|
|
|
43
78
|
const usage = supportedStopReason ? normalizeUsage(message.usage) : undefined;
|
|
44
79
|
const at = typeof row.timestamp === "string" && Number.isFinite(Date.parse(row.timestamp)) ? new Date(row.timestamp).toISOString() : undefined;
|
|
45
80
|
const servedModel = safeModelLabel(message.model);
|
|
46
|
-
latest.set(message.id, { ...(usage ? { usage } : {}), ...(at ? { at } : {}), ...(servedModel ? { servedModel } : {}) });
|
|
81
|
+
latest.set(message.id, { ...(usage ? { usage } : {}), ...(at ? { at } : {}), ...(servedModel ? { servedModel } : {}), ...(message.stop_reason === "end_turn" ? { completedTurn: true } : {}) });
|
|
47
82
|
} catch {
|
|
48
83
|
// A streaming or crash-truncated line is not a record.
|
|
49
84
|
}
|
package/src/models/relay.ts
CHANGED
|
@@ -324,6 +324,11 @@ function sseResponse(response: Response, release: () => void, onModel?: (model:
|
|
|
324
324
|
|
|
325
325
|
const copyRecord = (record: RelayRequestRecord): RelayRequestRecord => ({ ...record, ...(record.requestUsage ? { requestUsage: { ...record.requestUsage } } : {}), ...(record.toolSurface ? { toolSurface: { ...record.toolSurface, names: [...record.toolSurface.names] } } : {}) });
|
|
326
326
|
|
|
327
|
+
/** Pure advertised alias list, also used by no-start launch previews. */
|
|
328
|
+
export function relayModelIds(options: Pick<ModelRelayOptions, "enableHubAuto" | "mlx" | "mlxAlias" | "allowedDGXmodels">): string[] {
|
|
329
|
+
return [...new Set([...(options.enableHubAuto ? ["hub/auto"] : []), ...(options.mlx ? [options.mlxAlias ?? "mlx/fast"] : []), ...Object.keys(options.allowedDGXmodels)])];
|
|
330
|
+
}
|
|
331
|
+
|
|
327
332
|
export async function startModelRelay(options: ModelRelayOptions): Promise<ModelRelay> {
|
|
328
333
|
const host = options.host ?? "127.0.0.1";
|
|
329
334
|
assertLoopback(host);
|
|
@@ -331,7 +336,7 @@ export async function startModelRelay(options: ModelRelayOptions): Promise<Model
|
|
|
331
336
|
const mlxAlias = options.mlxAlias ?? "mlx/fast";
|
|
332
337
|
const dgxMaxInputTokens = options.dgxMaxInputTokens ?? 262_144;
|
|
333
338
|
const defaultBackend = options.defaultBackend ?? (options.mlx ? { kind: "mlx", alias: mlxAlias } : { kind: "dgx", alias: "dgx/coding" });
|
|
334
|
-
const models =
|
|
339
|
+
const models = relayModelIds(options);
|
|
335
340
|
const autoRoute = options.enableHubAuto ? new AutoRouteSelector(options, defaultBackend, mlxAlias, estimateInputTokens) : undefined;
|
|
336
341
|
let mlx: MlxHandle | undefined;
|
|
337
342
|
let mlxStarting: Promise<MlxHandle> | undefined;
|
package/src/pi/launch.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { peerChildEnv } from "../hub/child-process.ts";
|
|
3
|
+
import type { PiOptions, PiModelDescriptor, PiTuiLaunch } from "../adapters/pi.ts";
|
|
4
|
+
|
|
5
|
+
const modelFor = (backend: PiOptions["backend"], models: PiModelDescriptor[]): string => {
|
|
6
|
+
const ids = models.map((m) => m.id);
|
|
7
|
+
const find = (needle: string) => ids.find((id) => id === needle) ?? needle;
|
|
8
|
+
if (backend === "mlx") return find("mlx/fast");
|
|
9
|
+
if (backend === "dgx") return find("dgx/coding");
|
|
10
|
+
if (ids.includes("hub/auto")) return "hub/auto";
|
|
11
|
+
return find(ids.find((id) => id === "dgx/coding") ? "dgx/coding" : ids.find((id) => id === "mlx/fast") ?? "dgx/coding");
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Pure native launch; bridge/relay setup and session validation remain with the adapter. */
|
|
15
|
+
export function buildPiLaunch(opts: Pick<PiOptions, "stateDir" | "cmd" | "mode" | "backend" | "model" | "relay" | "tools" | "preamble" | "maxSteps" | "sessionFile" | "sessionId">, inherited: NodeJS.ProcessEnv, extension: string, bridgePort: number | string, bridgeToken: string): PiTuiLaunch {
|
|
16
|
+
const env: NodeJS.ProcessEnv = { ...peerChildEnv("pi", inherited), PI_CODING_AGENT_DIR: join(opts.stateDir, "pi"), AGENTHUB_PI_BRIDGE_URL: `http://127.0.0.1:${bridgePort}`, AGENTHUB_PI_BRIDGE_TOKEN: bridgeToken, AGENTHUB_PI_OWNER_TOKEN: bridgeToken, AGENTHUB_PI_RELAY_URL: opts.relay.url, AGENTHUB_PI_RELAY_TOKEN: opts.relay.token, AGENTHUB_PI_MODELS: JSON.stringify(opts.relay.models), AGENTHUB_PI_TOOLS: JSON.stringify(opts.tools), AGENTHUB_PI_MAX_STEPS: String(opts.maxSteps ?? 30) };
|
|
17
|
+
const args = [ ...(opts.mode === "headless" ? ["--mode", "rpc"] : []), "--provider", "agent-hub-local", "--model", opts.model ?? modelFor(opts.backend, opts.relay.models), "--models", opts.relay.models.map((model) => `agent-hub-local/${model.id}`).join(","), "--no-builtin-tools", "--no-skills", "--no-prompt-templates", "--no-extensions", "--extension", extension, ...(opts.preamble ? ["--append-system-prompt", opts.preamble] : []), "--session-dir", join(opts.stateDir, "pi-sessions")];
|
|
18
|
+
if (opts.sessionFile) args.push("--session", opts.sessionFile); else if (opts.sessionId) args.push("--session-id", opts.sessionId);
|
|
19
|
+
const command = opts.cmd ?? ["pi"];
|
|
20
|
+
return { cmd: command[0]!, args: [...command.slice(1), ...args], env };
|
|
21
|
+
}
|
package/src/ui/index.html
CHANGED
|
@@ -205,6 +205,12 @@ function render(snapshot) {
|
|
|
205
205
|
if (!Object.keys(snapshot.budget).length) return empty(target, 'No usage readings yet. Usage appears when a peer reports it.');
|
|
206
206
|
for (const [peer, budget] of Object.entries(snapshot.budget)) {
|
|
207
207
|
const row = el('div', undefined, 'row'); row.append(el('div', peer, 'label'));
|
|
208
|
+
const context = budget.context;
|
|
209
|
+
if (context) {
|
|
210
|
+
const pct = context.used === null ? null : Math.round(context.used * 100);
|
|
211
|
+
row.append(el('p', 'Context: ' + (pct === null ? 'unknown' : pct + '% used') + ' (' + context.freshness + ')' + (context.source ? ' / ' + context.source : '') + (context.measuredAt !== null ? ' / measured ' + time(context.measuredAt) : ''), 'meta'));
|
|
212
|
+
if (pct !== null) { const bar = el('progress'); bar.max = 1; bar.value = context.used; bar.setAttribute('aria-label', peer + ' context usage'); row.append(bar); }
|
|
213
|
+
}
|
|
208
214
|
if (budget.paused) { const line = el('p', 'Paused: ' + budget.paused.reason + ' / resets ', 'meta'), stamp = el('span'); stamp.dataset.ts = budget.paused.resetsAt; stamp.title = time(budget.paused.resetsAt); stamp.textContent = rel(budget.paused.resetsAt); line.append(stamp); row.append(line); }
|
|
209
215
|
if (!budget.windows.length) row.append(el('p', 'No usage windows reported.', 'meta'));
|
|
210
216
|
for (const window of budget.windows) {
|