@humwick/runner 0.0.0-stage → 0.130.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +167 -0
- package/agent-bin/agent-relay-browser +10 -0
- package/agent-bin/humwick +10 -0
- package/hooks/fff-search-redirect/fff-search-redirect.cjs +161 -0
- package/hooks/fff-search-redirect/hooks.json +13 -0
- package/hooks/rg-r-guard/hooks.json +18 -0
- package/hooks/rg-r-guard/rg-r-guard.cjs +144 -0
- package/hooks/web-tools-redirect/hooks.json +13 -0
- package/hooks/web-tools-redirect/web-tools-redirect.cjs +84 -0
- package/node_modules/@humwick/offload/LICENSE +167 -0
- package/node_modules/@humwick/offload/README.md +57 -0
- package/node_modules/@humwick/offload/dist/async-runner.d.ts +18 -0
- package/node_modules/@humwick/offload/dist/async-runner.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/async-runner.js +158 -0
- package/node_modules/@humwick/offload/dist/async-runner.js.map +1 -0
- package/node_modules/@humwick/offload/dist/envelope.d.ts +25 -0
- package/node_modules/@humwick/offload/dist/envelope.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/envelope.js +80 -0
- package/node_modules/@humwick/offload/dist/envelope.js.map +1 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts +10 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js +59 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js.map +1 -0
- package/node_modules/@humwick/offload/dist/index.d.ts +5 -0
- package/node_modules/@humwick/offload/dist/index.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/index.js +5 -0
- package/node_modules/@humwick/offload/dist/index.js.map +1 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts +62 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.js +295 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.js.map +1 -0
- package/node_modules/@humwick/offload/dist/types.d.ts +111 -0
- package/node_modules/@humwick/offload/dist/types.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/types.js +176 -0
- package/node_modules/@humwick/offload/dist/types.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts +23 -0
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-pool.js +435 -0
- package/node_modules/@humwick/offload/dist/worker-pool.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts +2 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.js +27 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker.d.ts +5 -0
- package/node_modules/@humwick/offload/dist/worker.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker.js +1 -0
- package/node_modules/@humwick/offload/dist/worker.js.map +1 -0
- package/node_modules/@humwick/offload/package.json +42 -0
- package/node_modules/@humwick/offload/src/async-runner.ts +170 -0
- package/node_modules/@humwick/offload/src/envelope.ts +92 -0
- package/node_modules/@humwick/offload/src/fixtures/worker-task.ts +49 -0
- package/node_modules/@humwick/offload/src/index.ts +4 -0
- package/node_modules/@humwick/offload/src/subprocess-runner.ts +334 -0
- package/node_modules/@humwick/offload/src/types.ts +235 -0
- package/node_modules/@humwick/offload/src/worker-pool.ts +463 -0
- package/node_modules/@humwick/offload/src/worker-runtime.ts +32 -0
- package/node_modules/@humwick/offload/src/worker.ts +5 -0
- package/package.json +47 -4
- package/plugins/browser/skills/playwright-cli/SKILL.md +53 -0
- package/plugins/claude/.claude-plugin/plugin.json +8 -0
- package/plugins/claude/claude-statusline-probe.cjs +144 -0
- package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +914 -0
- package/plugins/claude/hooks/gate-run-guard.sh +9 -0
- package/plugins/claude/hooks/gate-run-guard.ts +237 -0
- package/plugins/claude/hooks/hooks.json +158 -0
- package/plugins/claude/hooks/kill-guard.provisioned.mjs +7027 -0
- package/plugins/claude/hooks/kill-guard.sh +341 -0
- package/plugins/claude/hooks/kill-guard.ts +337 -0
- package/plugins/claude/hooks/permission-request.sh +22 -0
- package/plugins/claude/hooks/post-compact.sh +10 -0
- package/plugins/claude/hooks/post-tool-use.sh +8 -0
- package/plugins/claude/hooks/pre-compact.sh +12 -0
- package/plugins/claude/hooks/read-only-bash-guard.sh +587 -0
- package/plugins/claude/hooks/relay-status.sh +373 -0
- package/plugins/claude/hooks/session-end.sh +26 -0
- package/plugins/claude/hooks/session-start.sh +31 -0
- package/plugins/claude/hooks/shell-trust.ts +28 -0
- package/plugins/claude/hooks/stop-failure.sh +47 -0
- package/plugins/claude/hooks/stop.sh +48 -0
- package/plugins/claude/hooks/subagent-guard.cjs +26 -0
- package/plugins/claude/hooks/subagent-skill-policy.cjs +160 -0
- package/plugins/claude/hooks/subagent-start.sh +13 -0
- package/plugins/claude/hooks/subagent-stop.sh +19 -0
- package/plugins/claude/hooks/user-prompt-submit.sh +18 -0
- package/plugins/claude/hooks/wait-loop-guard.ts +120 -0
- package/plugins/claude/monitors/monitors.json +7 -0
- package/plugins/claude/monitors/relay-monitor.provisioned.mjs +786 -0
- package/plugins/claude/monitors/relay-monitor.ts +58 -0
- package/plugins/claude/skills/disconnect/SKILL.md +16 -0
- package/plugins/claude/skills/label/SKILL.md +23 -0
- package/plugins/claude/skills/message/SKILL.md +40 -0
- package/plugins/claude/skills/pair/SKILL.md +30 -0
- package/plugins/claude/skills/react/SKILL.md +18 -0
- package/plugins/claude/skills/read-message/SKILL.md +24 -0
- package/plugins/claude/skills/reply/SKILL.md +44 -0
- package/plugins/claude/skills/send-claimable/SKILL.md +39 -0
- package/plugins/claude/skills/slice-first/SKILL.md +32 -0
- package/plugins/claude/skills/slice-first/references/plan-recipe.md +46 -0
- package/plugins/claude/skills/status/SKILL.md +16 -0
- package/plugins/claude/skills/tags/SKILL.md +25 -0
- package/plugins/claude/skills/task/SKILL.md +28 -0
- package/plugins/claude-output-style/.claude-plugin/plugin.json +5 -0
- package/plugins/claude-output-style/output-styles/agent-relay.md +5 -0
- package/plugins/codex/skills/disconnect/SKILL.md +15 -0
- package/plugins/codex/skills/guide/SKILL.md +15 -0
- package/plugins/codex/skills/label/SKILL.md +22 -0
- package/plugins/codex/skills/message/SKILL.md +40 -0
- package/plugins/codex/skills/pair/SKILL.md +29 -0
- package/plugins/codex/skills/react/SKILL.md +17 -0
- package/plugins/codex/skills/read-message/SKILL.md +23 -0
- package/plugins/codex/skills/reply/SKILL.md +44 -0
- package/plugins/codex/skills/send-claimable/SKILL.md +39 -0
- package/plugins/codex/skills/slice-first/SKILL.md +32 -0
- package/plugins/codex/skills/slice-first/references/plan-recipe.md +46 -0
- package/plugins/codex/skills/status/SKILL.md +15 -0
- package/plugins/codex/skills/tags/SKILL.md +24 -0
- package/plugins/codex/skills/task/SKILL.md +28 -0
- package/src/adapter.ts +1061 -0
- package/src/adapters/__fixtures__/codex-app-server.ts +47 -0
- package/src/adapters/claude-api.ts +243 -0
- package/src/adapters/claude-context-command.ts +16 -0
- package/src/adapters/claude-credentials.ts +22 -0
- package/src/adapters/claude-delivery-lanes.ts +28 -0
- package/src/adapters/claude-delivery.ts +166 -0
- package/src/adapters/claude-effort.ts +94 -0
- package/src/adapters/claude-exit.ts +290 -0
- package/src/adapters/claude-headless-mirror.ts +88 -0
- package/src/adapters/claude-headless-refusal.ts +51 -0
- package/src/adapters/claude-headless.ts +617 -0
- package/src/adapters/claude-monitor-delivery-plan.ts +317 -0
- package/src/adapters/claude-permission.ts +243 -0
- package/src/adapters/claude-prompt-gates.ts +269 -0
- package/src/adapters/claude-prompt-hook.ts +145 -0
- package/src/adapters/claude-session-capture.ts +293 -0
- package/src/adapters/claude-session-probe.ts +414 -0
- package/src/adapters/claude-shutdown.ts +190 -0
- package/src/adapters/claude-status-detectors.ts +426 -0
- package/src/adapters/claude-subagents.ts +93 -0
- package/src/adapters/claude-submit-confirmation.ts +287 -0
- package/src/adapters/claude-tmux.ts +548 -0
- package/src/adapters/claude-token-usage-capture.ts +253 -0
- package/src/adapters/claude-tool-policy.ts +165 -0
- package/src/adapters/claude-transcript-tail.ts +193 -0
- package/src/adapters/claude-transcript.ts +812 -0
- package/src/adapters/claude.ts +1097 -0
- package/src/adapters/codex-agent-message-capture.ts +189 -0
- package/src/adapters/codex-app-server.ts +70 -0
- package/src/adapters/codex-client.ts +362 -0
- package/src/adapters/codex-effective-config.ts +87 -0
- package/src/adapters/codex-launch-config.ts +153 -0
- package/src/adapters/codex-rollout-context.ts +272 -0
- package/src/adapters/codex-skill-discovery.ts +18 -0
- package/src/adapters/codex-token-usage-capture.ts +163 -0
- package/src/adapters/codex-tool-output.ts +53 -0
- package/src/adapters/codex-turn-outcome.ts +27 -0
- package/src/adapters/codex-version.ts +64 -0
- package/src/adapters/codex.ts +1442 -0
- package/src/adapters/incremental-file.ts +110 -0
- package/src/adapters/opencode-api.ts +71 -0
- package/src/adapters/opencode-context.ts +277 -0
- package/src/adapters/opencode-model-registry.ts +126 -0
- package/src/adapters/opencode-provider.ts +169 -0
- package/src/adapters/opencode-server-retry.ts +50 -0
- package/src/adapters/opencode-soft-steer.ts +302 -0
- package/src/adapters/opencode-spawn.ts +145 -0
- package/src/adapters/opencode-subscribe.ts +62 -0
- package/src/adapters/opencode-token-usage-capture.ts +187 -0
- package/src/adapters/opencode-tool-events.ts +302 -0
- package/src/adapters/opencode-tools-config.ts +17 -0
- package/src/adapters/opencode.ts +2264 -0
- package/src/adapters/project-shell.ts +80 -0
- package/src/adapters/provider-launch-config.ts +21 -0
- package/src/adapters/provider-quota-harvest.ts +127 -0
- package/src/adapters/service-access.ts +153 -0
- package/src/adapters/tmux-offload.ts +207 -0
- package/src/adapters/tool-summary.ts +214 -0
- package/src/agent-redact.ts +75 -0
- package/src/agent-runner/commands/dispatch.ts +185 -0
- package/src/agent-runner/commands/lifecycle.ts +84 -0
- package/src/agent-runner/commands/prompt.ts +284 -0
- package/src/agent-runner/credentials.ts +208 -0
- package/src/agent-runner/http-liveness.ts +85 -0
- package/src/agent-runner/lifecycle-milestones.ts +207 -0
- package/src/agent-runner/lifecycle-phase.ts +80 -0
- package/src/agent-runner/registration.ts +163 -0
- package/src/agent-runner/runner-context.ts +158 -0
- package/src/agent-runner/session-mirror.ts +278 -0
- package/src/agent-runner/task-claims.ts +116 -0
- package/src/agent-runner/token-usage-coverage.ts +54 -0
- package/src/agent-runner/turn-report.ts +269 -0
- package/src/api-budget-accounting.ts +91 -0
- package/src/area-memory.ts +255 -0
- package/src/artifact-path-materialize.ts +52 -0
- package/src/async-guard.ts +35 -0
- package/src/attachment-cache-sweep.ts +31 -0
- package/src/attachment-cache.ts +195 -0
- package/src/background-script-reconcile.ts +106 -0
- package/src/bounded-log.ts +59 -0
- package/src/bounded-spawn-sync.ts +18 -0
- package/src/busy-reconciler.ts +282 -0
- package/src/claim-tracker.ts +140 -0
- package/src/claude-settings.ts +167 -0
- package/src/claude-workspace-trust.ts +289 -0
- package/src/config.ts +547 -0
- package/src/context-boundary.ts +101 -0
- package/src/context-command.ts +151 -0
- package/src/context-probe-state.ts +6 -0
- package/src/continuation-archive.ts +179 -0
- package/src/control-server.ts +730 -0
- package/src/delivery-policy.ts +9 -0
- package/src/delivery-result.ts +24 -0
- package/src/effort-command.ts +125 -0
- package/src/empty-turn.ts +816 -0
- package/src/free-port.ts +25 -0
- package/src/fs-offload.ts +26 -0
- package/src/gate-run-config.ts +35 -0
- package/src/gate-run-lease-probe.ts +38 -0
- package/src/host-asset-plan.ts +450 -0
- package/src/host-claude-config-worker.ts +34 -0
- package/src/host-sources.ts +40 -0
- package/src/inbox-carrier.ts +7 -0
- package/src/index.ts +386 -0
- package/src/launch-assembly.ts +955 -0
- package/src/liveness.ts +105 -0
- package/src/logger.ts +108 -0
- package/src/mcp-outbox.ts +204 -0
- package/src/mcp-preflight.ts +86 -0
- package/src/mcp-source-selection.ts +54 -0
- package/src/mcp-tool-deferral.ts +35 -0
- package/src/mcp-vault-env.ts +25 -0
- package/src/message-body-config/config.ts +16 -0
- package/src/message-body-config.ts +1 -0
- package/src/mid-turn-delivery.ts +43 -0
- package/src/native-memory-lever.ts +329 -0
- package/src/native-self-resume.ts +244 -0
- package/src/outbox.ts +453 -0
- package/src/park-post.ts +17 -0
- package/src/pending-permission-store.ts +148 -0
- package/src/process-meta.ts +9 -0
- package/src/profile-home.ts +419 -0
- package/src/profile-projection.ts +411 -0
- package/src/project-hooks.ts +65 -0
- package/src/project-instructions.ts +119 -0
- package/src/provider-double.ts +59 -0
- package/src/provider-hold.ts +228 -0
- package/src/provider-home-marker.ts +3 -0
- package/src/provider-home-tombstone.ts +4 -0
- package/src/provider-input-queue.ts +134 -0
- package/src/provider-integration.ts +93 -0
- package/src/provider-processes.ts +17 -0
- package/src/provider-submit-failure.ts +15 -0
- package/src/providers.ts +35 -0
- package/src/provisioning.ts +703 -0
- package/src/question-decision.ts +18 -0
- package/src/quota.ts +22 -0
- package/src/rate-limit.ts +351 -0
- package/src/refused-prompt-store.ts +36 -0
- package/src/relay-injection-events.ts +161 -0
- package/src/relay-instructions.ts +107 -0
- package/src/relay-mcp-proxy.long-turn-e2e.ts +183 -0
- package/src/relay-mcp-proxy.ts +704 -0
- package/src/relay-mcp.ts +583 -0
- package/src/reply-obligation-cache.ts +211 -0
- package/src/response-capture-report.ts +111 -0
- package/src/response-routing.ts +6 -0
- package/src/runner-core.ts +2955 -0
- package/src/runner-exit.ts +12 -0
- package/src/runner-helpers.ts +582 -0
- package/src/runner-insights.ts +105 -0
- package/src/runner-mcp-proxy.ts +46 -0
- package/src/runner-spawn-config.ts +68 -0
- package/src/runner-stale-scratch-sweep.ts +21 -0
- package/src/runner.ts +9 -0
- package/src/runtime-token-file.ts +45 -0
- package/src/runtime-token-remint.ts +56 -0
- package/src/service-access.ts +88 -0
- package/src/session-destroy.ts +27 -0
- package/src/session-insights.ts +118 -0
- package/src/session-scratch.ts +296 -0
- package/src/spawn-input.ts +147 -0
- package/src/startup-config-fetch.ts +48 -0
- package/src/subreaper.ts +113 -0
- package/src/template-resolver.ts +29 -0
- package/src/test-run-dedup.ts +435 -0
- package/src/transcript-context.ts +99 -0
- package/src/transport-errors.ts +48 -0
- package/src/turn-opener.ts +55 -0
- package/src/turn-reconcile-event.ts +88 -0
- package/src/vault-proxy-hop.ts +99 -0
- package/src/version.ts +44 -0
- package/src/web-tool-call-proxy.ts +128 -0
- package/src/web-tools.ts +59 -0
- package/README.md +0 -3
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { WorkspaceMetadata } from "@humwick/sdk";
|
|
2
|
+
import type { ManagedProcess, ProviderAdapter } from "./adapter";
|
|
3
|
+
import type { Outbox } from "./outbox";
|
|
4
|
+
import type { SessionDestroyReason } from "./session-destroy";
|
|
5
|
+
import { resolveProjectName } from "./config";
|
|
6
|
+
import { computeContextRatio } from "./session-insights";
|
|
7
|
+
import { boundContinuationArchiveSegment } from "./continuation-archive";
|
|
8
|
+
|
|
9
|
+
interface RunnerInsightsDeps {
|
|
10
|
+
agentId: string;
|
|
11
|
+
cwd: string;
|
|
12
|
+
workspace?: WorkspaceMetadata;
|
|
13
|
+
getOutbox(): Outbox;
|
|
14
|
+
getProviderSessionId(): string;
|
|
15
|
+
getProcess(): ManagedProcess | undefined;
|
|
16
|
+
getAdapter(): ProviderAdapter;
|
|
17
|
+
getLastTranscriptPath(): string | undefined;
|
|
18
|
+
sessionLog(message: string): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class RunnerInsights {
|
|
22
|
+
private insightsObserved = 0;
|
|
23
|
+
private insightsCursorKey = "";
|
|
24
|
+
private archiveObservedChars = 0;
|
|
25
|
+
private archiveCursorKey = "";
|
|
26
|
+
private projectName?: string;
|
|
27
|
+
|
|
28
|
+
constructor(private readonly deps: RunnerInsightsDeps) {}
|
|
29
|
+
|
|
30
|
+
project(): string {
|
|
31
|
+
return (this.projectName ??= resolveProjectName(this.deps.cwd, this.deps.workspace));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
queueHookFatal(report: { hook: string; error: string }): void {
|
|
35
|
+
this.deps.getOutbox().enqueue({
|
|
36
|
+
kind: "insight",
|
|
37
|
+
payload: {
|
|
38
|
+
sessionId: this.deps.getProviderSessionId(),
|
|
39
|
+
project: this.project(),
|
|
40
|
+
agentId: this.deps.agentId,
|
|
41
|
+
signal: "hook_fatal",
|
|
42
|
+
value: { hook: report.hook, error: report.error },
|
|
43
|
+
source: "server",
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async captureContinuationArchive(reason: SessionDestroyReason, opts?: { transcriptPath?: string }): Promise<void> {
|
|
49
|
+
const adapter = this.deps.getAdapter();
|
|
50
|
+
const process = this.deps.getProcess();
|
|
51
|
+
if (!adapter.collectSessionArchiveSegment || !process) return;
|
|
52
|
+
const transcriptPath = opts?.transcriptPath ?? this.deps.getLastTranscriptPath();
|
|
53
|
+
const archive = await adapter.collectSessionArchiveSegment(process, { transcriptPath });
|
|
54
|
+
if (!archive) return;
|
|
55
|
+
const key = transcriptPath ?? `session:${this.deps.getProviderSessionId()}`;
|
|
56
|
+
if (key !== this.archiveCursorKey || archive.length < this.archiveObservedChars) {
|
|
57
|
+
this.archiveCursorKey = key;
|
|
58
|
+
this.archiveObservedChars = 0;
|
|
59
|
+
}
|
|
60
|
+
const segment = archive.slice(this.archiveObservedChars).trim();
|
|
61
|
+
this.archiveObservedChars = archive.length;
|
|
62
|
+
if (!segment) return;
|
|
63
|
+
const bounded = boundContinuationArchiveSegment(segment);
|
|
64
|
+
if (bounded.droppedBytes > 0) this.deps.sessionLog(`continuation archive truncated at ${bounded.keptBytes} bytes; dropped ${bounded.droppedBytes} bytes (${reason})`);
|
|
65
|
+
this.deps.getOutbox().enqueue({
|
|
66
|
+
kind: "continuation-archive",
|
|
67
|
+
payload: {
|
|
68
|
+
agentId: this.deps.agentId,
|
|
69
|
+
segment: bounded.segment,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
this.deps.sessionLog(`continuation archive queued (${bounded.segment.length} chars, ${reason})`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async captureContextRatio(reason: SessionDestroyReason, opts?: { transcriptPath?: string }): Promise<void> {
|
|
76
|
+
const adapter = this.deps.getAdapter();
|
|
77
|
+
const process = this.deps.getProcess();
|
|
78
|
+
if (!adapter.collectSessionEvents || !process) return;
|
|
79
|
+
const transcriptPath = opts?.transcriptPath ?? this.deps.getLastTranscriptPath();
|
|
80
|
+
const events = await adapter.collectSessionEvents(process, { transcriptPath });
|
|
81
|
+
if (!events) return;
|
|
82
|
+
const key = transcriptPath ?? `session:${this.deps.getProviderSessionId()}`;
|
|
83
|
+
if (key !== this.insightsCursorKey || events.length < this.insightsObserved) {
|
|
84
|
+
this.insightsCursorKey = key;
|
|
85
|
+
this.insightsObserved = 0;
|
|
86
|
+
}
|
|
87
|
+
const segment = events.slice(this.insightsObserved);
|
|
88
|
+
this.insightsObserved = events.length;
|
|
89
|
+
const analysis = computeContextRatio(segment);
|
|
90
|
+
if (!analysis) return;
|
|
91
|
+
this.deps.getOutbox().enqueue({
|
|
92
|
+
kind: "insight",
|
|
93
|
+
payload: {
|
|
94
|
+
sessionId: this.deps.getProviderSessionId(),
|
|
95
|
+
project: this.project(),
|
|
96
|
+
agentId: this.deps.agentId,
|
|
97
|
+
signal: "context_ratio",
|
|
98
|
+
value: { ...analysis.metric, endReason: reason },
|
|
99
|
+
outcome: { ...analysis.outcome },
|
|
100
|
+
source: "server",
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
this.deps.sessionLog(`insights: context_ratio ${analysis.metric.ratio.toFixed(2)} (${analysis.metric.gatheringCalls}/${analysis.metric.totalToolCalls} gathering, ${reason}) queued`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { sharedCallmuxToolCallTimeoutMsFromEnv, type HumwickHttpClient, webSearchEnabledFromEnv, webCallLogEnabledFromEnv, errMessage, type WebToolCall, type AgentProfile } from "@humwick/sdk";
|
|
2
|
+
import { RelayMcpProxy, type RelayMcpProxyOptions } from "./relay-mcp-proxy";
|
|
3
|
+
import { runnerManagedRelayOnDemandTools } from "./mcp-tool-deferral";
|
|
4
|
+
import { logger } from "./logger";
|
|
5
|
+
import type { WebToolLogOptions } from "./web-tool-call-proxy";
|
|
6
|
+
|
|
7
|
+
export function runnerMcpHiddenTools(provider: string, profile: AgentProfile | undefined): Set<string> | undefined {
|
|
8
|
+
const hidden = runnerManagedRelayOnDemandTools(provider, profile);
|
|
9
|
+
return profile?.webTools?.brave === false ? new Set([...(hidden ?? []), "web_search"]) : hidden;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function startRunnerMcpProxy(options: RelayMcpProxyOptions): { proxy?: RelayMcpProxy; endpoint?: string } {
|
|
13
|
+
try {
|
|
14
|
+
const proxy = new RelayMcpProxy(options);
|
|
15
|
+
const endpoint = proxy.start().url;
|
|
16
|
+
logger.info("mcp-proxy", `runner MCP proxy listening at ${endpoint} (worktree=${options.initialContext?.isolatedWorktree ?? false})`);
|
|
17
|
+
return { proxy, endpoint };
|
|
18
|
+
} catch (error) {
|
|
19
|
+
logger.warn("mcp-proxy", `failed to start MCP proxy; agent will connect to the relay directly: ${errMessage(error)}`);
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function runnerWebToolLog(
|
|
25
|
+
enabled: boolean,
|
|
26
|
+
upstreamUrl: string,
|
|
27
|
+
identity: () => Pick<WebToolCall, "agentId" | "provider" | "model" | "effort">,
|
|
28
|
+
record: (call: WebToolCall) => Promise<Response>,
|
|
29
|
+
env: Record<string, string | undefined> = process.env,
|
|
30
|
+
search?: WebToolLogOptions["search"],
|
|
31
|
+
): WebToolLogOptions | undefined {
|
|
32
|
+
if (!enabled || (!webCallLogEnabledFromEnv(env) && !webSearchEnabledFromEnv(env))) return undefined;
|
|
33
|
+
return { upstreamUrl, ...(webSearchEnabledFromEnv(env) && search ? { search } : {}), record: async row => {
|
|
34
|
+
if (!webCallLogEnabledFromEnv(env)) return;
|
|
35
|
+
const response = await record({ ...row, ...identity() });
|
|
36
|
+
const text = await response.text();
|
|
37
|
+
if (!response.ok) throw new Error(`web call observation rejected (${response.status}): ${text.slice(0, 1000)}`);
|
|
38
|
+
} };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Search includes both backend attempts; the ordinary HTTP default is too short. */
|
|
42
|
+
export function runnerWebSearch(http: Pick<HumwickHttpClient, "request">, env: Record<string, string | undefined> = process.env): NonNullable<WebToolLogOptions["search"]> {
|
|
43
|
+
return (args, id, signal) => http.request("POST", "/api/mcp", {
|
|
44
|
+
jsonrpc: "2.0", id, method: "tools/call", params: { name: "web_search", arguments: args },
|
|
45
|
+
}, { timeoutMs: sharedCallmuxToolCallTimeoutMsFromEnv(env), signal });
|
|
46
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { writeChildEnv } from "@humwick/sdk/env";
|
|
2
|
+
import { harnessSubagentsEnabled } from "@humwick/sdk";
|
|
3
|
+
import type { RunnerSpawnConfig } from "./adapter";
|
|
4
|
+
|
|
5
|
+
export type RunnerSpawnConfigSource = Pick<RunnerSpawnConfig,
|
|
6
|
+
| "provider" | "model" | "effort" | "contextWindowTokens" | "modelRecord"
|
|
7
|
+
| "runnerId" | "instanceId" | "relayUrl" | "cwd" | "headless" | "lifecycle"
|
|
8
|
+
| "approvalMode" | "label" | "rig" | "profile" | "harnessSubagents" | "agentProfile"
|
|
9
|
+
| "prompt" | "systemPromptAppend" | "tmuxSession" | "providerArgs" | "providerConfig"
|
|
10
|
+
>;
|
|
11
|
+
|
|
12
|
+
export interface RunnerSpawnRuntime {
|
|
13
|
+
getRelayToken?: () => string | undefined;
|
|
14
|
+
agentId: string;
|
|
15
|
+
env: Record<string, string>;
|
|
16
|
+
controlPort: number;
|
|
17
|
+
resumeId?: string;
|
|
18
|
+
suppressPrompt?: boolean;
|
|
19
|
+
systemPromptAppend?: string;
|
|
20
|
+
relayMcpEndpoint?: string;
|
|
21
|
+
mcpProxySecret?: string;
|
|
22
|
+
sharedMcpUrl?: string;
|
|
23
|
+
monitor?: RunnerSpawnConfig["monitor"];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Production-shared, pure construction of the exact config passed to provider adapters. */
|
|
27
|
+
export function buildRunnerSpawnConfig(
|
|
28
|
+
source: RunnerSpawnConfigSource,
|
|
29
|
+
runtime: RunnerSpawnRuntime,
|
|
30
|
+
): RunnerSpawnConfig {
|
|
31
|
+
const prompt = source.prompt && !runtime.resumeId && !runtime.suppressPrompt ? source.prompt : undefined;
|
|
32
|
+
const systemPromptAppend = runtime.systemPromptAppend ?? source.systemPromptAppend;
|
|
33
|
+
return {
|
|
34
|
+
provider: source.provider,
|
|
35
|
+
...(source.model ? { model: source.model } : {}),
|
|
36
|
+
...(source.effort ? { effort: source.effort } : {}),
|
|
37
|
+
// #2595 — positive-only gate mirrors the CLI parse; an unset/invalid value stays absent.
|
|
38
|
+
...(typeof source.contextWindowTokens === "number" && Number.isSafeInteger(source.contextWindowTokens) && source.contextWindowTokens > 0 ? { contextWindowTokens: source.contextWindowTokens } : {}),
|
|
39
|
+
// #2599 — the record arrives pre-parsed by the shared parser at the CLI seam; thread it
|
|
40
|
+
// verbatim. The ADAPTER applies precedence (payload > manifest > undefined), not this builder.
|
|
41
|
+
...(source.modelRecord ? { modelRecord: source.modelRecord } : {}),
|
|
42
|
+
runnerId: source.runnerId,
|
|
43
|
+
instanceId: source.instanceId,
|
|
44
|
+
agentId: runtime.agentId,
|
|
45
|
+
...(runtime.getRelayToken ? { getRelayToken: runtime.getRelayToken } : {}),
|
|
46
|
+
relayUrl: source.relayUrl,
|
|
47
|
+
cwd: source.cwd,
|
|
48
|
+
headless: source.headless,
|
|
49
|
+
...(source.lifecycle ? { lifecycle: source.lifecycle } : {}),
|
|
50
|
+
approvalMode: source.approvalMode,
|
|
51
|
+
...(source.label ? { label: source.label } : {}),
|
|
52
|
+
...(source.rig ? { rig: source.rig } : {}),
|
|
53
|
+
...(source.profile ? { profile: source.profile } : {}),
|
|
54
|
+
harnessSubagents: harnessSubagentsEnabled(source),
|
|
55
|
+
...(source.agentProfile ? { agentProfile: source.agentProfile } : {}),
|
|
56
|
+
...(prompt ? { prompt } : {}),
|
|
57
|
+
...(systemPromptAppend ? { systemPromptAppend } : {}),
|
|
58
|
+
...(source.tmuxSession ? { tmuxSession: source.tmuxSession } : {}),
|
|
59
|
+
providerArgs: runtime.resumeId ? [...source.providerArgs, "--resume", runtime.resumeId] : [...source.providerArgs],
|
|
60
|
+
providerConfig: source.providerConfig,
|
|
61
|
+
env: writeChildEnv({}, { ...runtime.env }),
|
|
62
|
+
controlPort: runtime.controlPort,
|
|
63
|
+
...(runtime.relayMcpEndpoint ? { relayMcpEndpoint: runtime.relayMcpEndpoint } : {}),
|
|
64
|
+
...(runtime.mcpProxySecret ? { mcpProxySecret: runtime.mcpProxySecret } : {}),
|
|
65
|
+
...(runtime.sharedMcpUrl ? { sharedMcpUrl: runtime.sharedMcpUrl } : {}),
|
|
66
|
+
...(runtime.monitor ? { monitor: runtime.monitor } : {}),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { runnerFsOffload } from "./fs-offload";
|
|
2
|
+
import { sweepStaleLaunchersAsync, sweepStaleSessionsAsync } from "./session-scratch";
|
|
3
|
+
|
|
4
|
+
export async function sweepRunnerStaleScratch(input: {
|
|
5
|
+
listAgents(): Promise<Array<{ id: string }>>;
|
|
6
|
+
agentId: string;
|
|
7
|
+
cwd: string;
|
|
8
|
+
fallbackBaseDir?: string;
|
|
9
|
+
log(message: string): void;
|
|
10
|
+
}): Promise<void> {
|
|
11
|
+
try {
|
|
12
|
+
const agents = await input.listAgents().catch(() => []);
|
|
13
|
+
const keep = new Set(agents.map((agent) => agent.id)); keep.add(input.agentId); const now = Date.now();
|
|
14
|
+
const [sessions, launchers] = await Promise.all([
|
|
15
|
+
runnerFsOffload().run("runner-stale-session-scratch-sweep", () => sweepStaleSessionsAsync({ cwd: input.cwd, fallbackBaseDir: input.fallbackBaseDir, keepAgentIds: keep, now, maxEntries: 100 })),
|
|
16
|
+
runnerFsOffload().run("runner-stale-launcher-sweep", () => sweepStaleLaunchersAsync({ keepAgentIds: keep, now, maxEntries: 100 })),
|
|
17
|
+
]);
|
|
18
|
+
if (sessions.length) input.log(`swept ${sessions.length} stale session scratch dir(s)`);
|
|
19
|
+
if (launchers.length) input.log(`swept ${launchers.length} stale launcher script(s)`);
|
|
20
|
+
} catch {}
|
|
21
|
+
}
|
package/src/runner.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { AgentRunner, type RunnerOptions } from "./runner-core";
|
|
2
|
+
export {
|
|
3
|
+
latestResumeIdFromText,
|
|
4
|
+
runnerMessageMatches,
|
|
5
|
+
runnerShouldResolveProviderExit,
|
|
6
|
+
runnerShouldRestartUnexpectedProviderExit,
|
|
7
|
+
runtimeTokenRenewDelayMs,
|
|
8
|
+
runtimeTokenRenewTimerSchedule,
|
|
9
|
+
} from "./runner-helpers";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { runtimeTokenExpiresAtFileFromEnv, runtimeTokenFileFromEnv } from "./config";
|
|
4
|
+
|
|
5
|
+
function replaceFile(path: string, value: string): void {
|
|
6
|
+
const tmp = `${path}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
7
|
+
try {
|
|
8
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
9
|
+
writeFileSync(tmp, value, { mode: 0o600 });
|
|
10
|
+
chmodSync(tmp, 0o600);
|
|
11
|
+
renameSync(tmp, path);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
try { rmSync(tmp, { force: true }); } catch {}
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Publish the runner's live credential to CLI children. A provider's process
|
|
20
|
+
* environment is immutable after it starts, but each CLI invocation can read
|
|
21
|
+
* these stable paths afresh. The orchestrator creates the initial files when
|
|
22
|
+
* it materializes the session launch script (#2318).
|
|
23
|
+
*/
|
|
24
|
+
export function persistRuntimeTokenFiles(token: string, expiresAt?: number): void {
|
|
25
|
+
const tokenFile = runtimeTokenFileFromEnv();
|
|
26
|
+
if (tokenFile) replaceFile(tokenFile, token);
|
|
27
|
+
const expiresAtFile = runtimeTokenExpiresAtFileFromEnv();
|
|
28
|
+
if (expiresAtFile && expiresAt !== undefined) replaceFile(expiresAtFile, String(expiresAt));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Keep CLI credential publication best-effort: failure to update a provider child
|
|
33
|
+
* must never undo a successfully renewed runner transport credential.
|
|
34
|
+
*/
|
|
35
|
+
export function publishRuntimeTokenForCli(
|
|
36
|
+
token: string,
|
|
37
|
+
expiresAt: number | undefined,
|
|
38
|
+
reportFailure: (message: string) => void,
|
|
39
|
+
): void {
|
|
40
|
+
try {
|
|
41
|
+
persistRuntimeTokenFiles(token, expiresAt);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
reportFailure(`[runner] failed to publish renewed runtime token for CLI: ${String(error)}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { errMessage, type HumwickHttpClient } from "@humwick/sdk";
|
|
2
|
+
|
|
3
|
+
type RemintedRecord = { jti: string; profileId?: string; expiresAt?: number };
|
|
4
|
+
|
|
5
|
+
/** One token rotation at a time: a renewal, an orchestrator re-mint and a boundary re-derive replace the same token. */
|
|
6
|
+
export class TokenRotation {
|
|
7
|
+
private current?: Promise<unknown>;
|
|
8
|
+
|
|
9
|
+
get inFlight(): boolean {
|
|
10
|
+
return this.current !== undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async run<T>(rotate: () => Promise<T>): Promise<T> {
|
|
14
|
+
const rotation = rotate();
|
|
15
|
+
this.current = rotation;
|
|
16
|
+
try {
|
|
17
|
+
return await rotation;
|
|
18
|
+
} finally {
|
|
19
|
+
if (this.current === rotation) this.current = undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Runs `rotate` once no rotation is in flight, so it starts from the token the previous one installed. */
|
|
24
|
+
async runNext<T>(rotate: () => Promise<T>): Promise<T> {
|
|
25
|
+
while (this.current) await this.current.catch(() => {});
|
|
26
|
+
return this.run(rotate);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* #1245 — re-derive the runner's token from the agent's CURRENT profile at a clear/compact boundary and
|
|
32
|
+
* report the outcome for the command result. Never throws: a failed re-mint keeps the still-valid token.
|
|
33
|
+
*/
|
|
34
|
+
export async function rederiveRuntimeToken(
|
|
35
|
+
http: Pick<HumwickHttpClient, "remintRuntimeToken">,
|
|
36
|
+
reason: "clear" | "compact",
|
|
37
|
+
apply: (token: string, record: RemintedRecord) => void,
|
|
38
|
+
log: (message: string) => void,
|
|
39
|
+
): Promise<Record<string, unknown>> {
|
|
40
|
+
let outcome: Record<string, unknown> | undefined;
|
|
41
|
+
try {
|
|
42
|
+
// A replayed successor (an earlier response was lost) can predate the current profile, so derive once more from it.
|
|
43
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
44
|
+
const result = await http.remintRuntimeToken(reason);
|
|
45
|
+
if ("unchanged" in result) return outcome ?? { status: "unchanged", jti: result.jti };
|
|
46
|
+
apply(result.token, result.record);
|
|
47
|
+
log(`[runner] runtime token re-derived at ${reason} (jti ${result.previous.jti} -> ${result.record.jti}${result.replayed ? ", replayed" : ""})`);
|
|
48
|
+
outcome = { status: "rotated", jti: result.record.jti, previousJti: result.previous.jti, ...result.delta };
|
|
49
|
+
if (!result.replayed) break;
|
|
50
|
+
}
|
|
51
|
+
return outcome!;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
log(`[runner] runtime token re-derive at ${reason} failed: ${errMessage(error)}`);
|
|
54
|
+
return { ...outcome, status: outcome ? "rotated" : "failed", error: errMessage(error) };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { rootCertificates } from "node:tls";
|
|
2
|
+
import type { ServiceAccessHandoff } from "@humwick/sdk";
|
|
3
|
+
import type { HumwickHttpClient } from "@humwick/sdk/http-client";
|
|
4
|
+
import type { ProviderAdapter, RunnerSpawnConfig } from "./adapter";
|
|
5
|
+
import { prepareServiceAccessConfig } from "./adapters/service-access";
|
|
6
|
+
import { fireAndForget } from "./async-guard";
|
|
7
|
+
import { startVaultProxyHop } from "./vault-proxy-hop";
|
|
8
|
+
|
|
9
|
+
type LaunchHttp = Pick<HumwickHttpClient, "request">;
|
|
10
|
+
type LaunchIdentity = { instanceId: string; launchId: string };
|
|
11
|
+
|
|
12
|
+
const RENEW_RETRY_MS = 60_000;
|
|
13
|
+
/** One provider launch per runner; a replacement launch closes its predecessor's hop and renewal. */
|
|
14
|
+
const liveLaunches = new WeakMap<object, { close(): void }>();
|
|
15
|
+
|
|
16
|
+
/** Launch only: transport reconnects must keep the child's original token. */
|
|
17
|
+
export async function withServiceAccessLaunch<T>(
|
|
18
|
+
http: LaunchHttp,
|
|
19
|
+
identity: LaunchIdentity,
|
|
20
|
+
enabled: boolean,
|
|
21
|
+
launch: (handoff?: ServiceAccessHandoff) => Promise<T>,
|
|
22
|
+
): Promise<T> {
|
|
23
|
+
if (!enabled) return launch();
|
|
24
|
+
try {
|
|
25
|
+
const response = await http.request("POST", "/api/service-access/exchange", identity, { timeoutMs: 30_000 });
|
|
26
|
+
if (!response.ok) throw new Error(`Vault launch exchange failed (${response.status})`);
|
|
27
|
+
const handoff = await response.json() as ServiceAccessHandoff | null;
|
|
28
|
+
return await launch(handoff ?? undefined);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
await http.request("POST", "/api/service-access/release", identity).catch(() => {});
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* #3577: the provider gets a loopback proxy URL and local credential for its whole life; the session
|
|
38
|
+
* token behind it renews at half its remaining lifetime (retrying each minute) until the launch ends.
|
|
39
|
+
* A 409 means the launch is no longer live, so exit cleanup owns the session from there.
|
|
40
|
+
*/
|
|
41
|
+
export async function startServiceAccessRenewal(http: LaunchHttp, identity: LaunchIdentity, handoff: ServiceAccessHandoff) {
|
|
42
|
+
const hop = await startVaultProxyHop(handoff.proxyUrl, handoff.vault, handoff.token, handoff.caPem ? [...rootCertificates, handoff.caPem] : undefined);
|
|
43
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
44
|
+
let closed = false;
|
|
45
|
+
const schedule = (delayMs: number) => {
|
|
46
|
+
if (closed) return;
|
|
47
|
+
timer = setTimeout(() => fireAndForget("vault access renewal", renew), Math.max(0, delayMs));
|
|
48
|
+
timer.unref?.();
|
|
49
|
+
};
|
|
50
|
+
const renew = async () => {
|
|
51
|
+
try {
|
|
52
|
+
const response = await http.request("POST", "/api/service-access/renew", { ...identity, tokenId: handoff.tokenId }, { timeoutMs: 30_000 });
|
|
53
|
+
if (response.status === 409) return;
|
|
54
|
+
if (!response.ok) throw new Error(`Vault access renewal failed (${response.status})`);
|
|
55
|
+
const renewed = await response.json() as { token: string; expiresAt: number };
|
|
56
|
+
if (closed) return;
|
|
57
|
+
hop.setToken(renewed.token);
|
|
58
|
+
schedule((renewed.expiresAt - Date.now()) / 2);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.warn(`[agent-relay] ${error instanceof Error ? error.message : "Vault access renewal failed"}; retrying in ${RENEW_RETRY_MS / 1000}s`);
|
|
61
|
+
schedule(RENEW_RETRY_MS);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
schedule((handoff.expiresAt - Date.now()) / 2);
|
|
65
|
+
return {
|
|
66
|
+
handoff: { ...handoff, proxyUrl: hop.url, token: hop.secret },
|
|
67
|
+
close() { closed = true; clearTimeout(timer); hop.close(); },
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function spawnWithServiceAccess(http: LaunchHttp, config: RunnerSpawnConfig, adapter: ProviderAdapter, launchId: string, managed: boolean) {
|
|
72
|
+
liveLaunches.get(http)?.close();
|
|
73
|
+
liveLaunches.delete(http);
|
|
74
|
+
const identity = { instanceId: config.instanceId, launchId };
|
|
75
|
+
return withServiceAccessLaunch(http, identity, managed, async (handoff) => {
|
|
76
|
+
const renewal = handoff ? await startServiceAccessRenewal(http, identity, handoff) : undefined;
|
|
77
|
+
try {
|
|
78
|
+
const launchConfig = prepareServiceAccessConfig({ ...config, ...(renewal ? { serviceAccess: renewal.handoff } : {}) });
|
|
79
|
+
const hydratedConfig = await adapter.hydrateSpawnConfig?.(launchConfig) ?? launchConfig;
|
|
80
|
+
const managedProcess = await adapter.spawn(hydratedConfig);
|
|
81
|
+
if (renewal) liveLaunches.set(http, renewal);
|
|
82
|
+
return { hydratedConfig, managedProcess };
|
|
83
|
+
} catch (error) {
|
|
84
|
+
renewal?.close();
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type SessionDestroyReason = "compact" | "clear" | "restart" | "shutdown" | "kill" | "crash";
|
|
2
|
+
|
|
3
|
+
export type LifecycleAction =
|
|
4
|
+
| "shutting-down" | "killing" | "restarting"
|
|
5
|
+
// #1746 — a TERMINAL marker: the graceful+force-kill path ran and the provider process
|
|
6
|
+
// still could not be confirmed dead. The runner deliberately keeps the agent registered
|
|
7
|
+
// (a roster removal must imply the process is gone), so this pill replaces an indefinite
|
|
8
|
+
// "shutting-down" with an operator-visible "the kill did not take" state.
|
|
9
|
+
| "kill-failed"
|
|
10
|
+
| `finalizing-${SessionDestroyReason}`;
|
|
11
|
+
|
|
12
|
+
export const PRE_DESTROY_TIMEOUT_MS = 4_000;
|
|
13
|
+
|
|
14
|
+
export function reasonExitsRunner(reason: SessionDestroyReason): boolean {
|
|
15
|
+
return reason === "shutdown" || reason === "kill" || reason === "crash";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function boundaryReasonForCommand(type: string): SessionDestroyReason | undefined {
|
|
19
|
+
switch (type) {
|
|
20
|
+
case "agent.compact": return "compact";
|
|
21
|
+
case "agent.clearContext": return "clear";
|
|
22
|
+
case "agent.restart": return "restart";
|
|
23
|
+
case "agent.shutdown": return "shutdown";
|
|
24
|
+
case "agent.kill": return "kill";
|
|
25
|
+
default: return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Provider-agnostic core for the #184 context-gathering signal (epic #183).
|
|
2
|
+
//
|
|
3
|
+
// The transcript *format* is provider-specific (Claude JSONL, Codex app-server items,
|
|
4
|
+
// future providers), so each adapter normalizes its session into the same `SessionEvent`
|
|
5
|
+
// stream via `collectSessionEvents`. Everything downstream — the gathering/action
|
|
6
|
+
// classifier and the ratio math — lives here once and is shared, so a tool reclassified
|
|
7
|
+
// for one provider is reclassified for all, and a new provider only implements the
|
|
8
|
+
// normalization.
|
|
9
|
+
//
|
|
10
|
+
// The classifier is model-free and runs in the runner, so it costs zero agent tokens and
|
|
11
|
+
// the agent can't game it.
|
|
12
|
+
|
|
13
|
+
// A normalized, ordered session event. Order is significant: `leadingGather` counts the
|
|
14
|
+
// run of gathering tools before the first action.
|
|
15
|
+
export type SessionEvent =
|
|
16
|
+
// A tool invocation. Gathering-vs-action is decided here by `isGatheringTool(name)`.
|
|
17
|
+
| { type: "tool"; name: string }
|
|
18
|
+
// A failed tool result (paired outcome proxy — failures/workarounds the agent hit).
|
|
19
|
+
| { type: "tool_error" }
|
|
20
|
+
// A real user prompt (paired outcome proxy — more back-and-forth ~ clarification/correction).
|
|
21
|
+
| { type: "user_prompt" }
|
|
22
|
+
// A substantive assistant turn (one that produced text or a tool call).
|
|
23
|
+
| { type: "turn" };
|
|
24
|
+
|
|
25
|
+
// Tools that acquire context without changing anything. Anything not matched here is
|
|
26
|
+
// treated as an action (mutation, execution, or a delegation/direction decision) — Bash
|
|
27
|
+
// counts as an action because it executes (a conservative, documented choice for v0;
|
|
28
|
+
// `cat`/`ls` via Bash are misclassified, refine later if the data warrants it).
|
|
29
|
+
const GATHERING_TOOLS = new Set([
|
|
30
|
+
"Read", "Grep", "Glob", "LS", "NotebookRead", "WebFetch", "WebSearch",
|
|
31
|
+
]);
|
|
32
|
+
const GATHERING_NAME = /(?:^|[._-])(read|get|list|search|grep|glob|find|fetch|query|browse|view|show|cat|status|inspect|lookup|symbols|snippet)/i;
|
|
33
|
+
|
|
34
|
+
export function isGatheringTool(name: string): boolean {
|
|
35
|
+
if (GATHERING_TOOLS.has(name)) return true;
|
|
36
|
+
// MCP / custom tools: classify by name shape (e.g. mcp__callmux__brave__brave_web_search).
|
|
37
|
+
return GATHERING_NAME.test(name);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ContextRatioMetric {
|
|
41
|
+
/** Session-wide gathering fraction: gatheringCalls / totalToolCalls. The headline metric. */
|
|
42
|
+
ratio: number;
|
|
43
|
+
gatheringCalls: number;
|
|
44
|
+
actionCalls: number;
|
|
45
|
+
totalToolCalls: number;
|
|
46
|
+
/** Consecutive gathering calls before the first action — the "read N files before moving" signal. */
|
|
47
|
+
leadingGather: number;
|
|
48
|
+
/** Substantive assistant turns (turns that produced text or a tool call). */
|
|
49
|
+
turns: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface SessionOutcomeProxy {
|
|
53
|
+
/** Real user prompts in the session — more back-and-forth ~ more clarification/correction. */
|
|
54
|
+
userPrompts: number;
|
|
55
|
+
/** tool_result blocks flagged is_error — failures/workarounds the agent hit. */
|
|
56
|
+
toolErrors: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface SessionAnalysis {
|
|
60
|
+
metric: ContextRatioMetric;
|
|
61
|
+
outcome: SessionOutcomeProxy;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Reduce a normalized event stream to the context-gathering ratio plus paired outcome
|
|
66
|
+
* proxies. Returns null when there's nothing substantive to measure (no tool calls) —
|
|
67
|
+
* trivial segments have nothing to learn from and shouldn't pollute the baselines.
|
|
68
|
+
*
|
|
69
|
+
* Per-segment by construction: callers pass only the events since the last capture
|
|
70
|
+
* boundary (compact/clear/restart/shutdown), so each result describes one work chunk.
|
|
71
|
+
*/
|
|
72
|
+
export function computeContextRatio(events: SessionEvent[]): SessionAnalysis | null {
|
|
73
|
+
let gatheringCalls = 0;
|
|
74
|
+
let actionCalls = 0;
|
|
75
|
+
let leadingGather = 0;
|
|
76
|
+
let sawAction = false;
|
|
77
|
+
let userPrompts = 0;
|
|
78
|
+
let toolErrors = 0;
|
|
79
|
+
let turns = 0;
|
|
80
|
+
|
|
81
|
+
for (const event of events) {
|
|
82
|
+
switch (event.type) {
|
|
83
|
+
case "user_prompt":
|
|
84
|
+
userPrompts++;
|
|
85
|
+
break;
|
|
86
|
+
case "tool_error":
|
|
87
|
+
toolErrors++;
|
|
88
|
+
break;
|
|
89
|
+
case "turn":
|
|
90
|
+
turns++;
|
|
91
|
+
break;
|
|
92
|
+
case "tool":
|
|
93
|
+
if (isGatheringTool(event.name)) {
|
|
94
|
+
gatheringCalls++;
|
|
95
|
+
if (!sawAction) leadingGather++;
|
|
96
|
+
} else {
|
|
97
|
+
actionCalls++;
|
|
98
|
+
sawAction = true;
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const totalToolCalls = gatheringCalls + actionCalls;
|
|
105
|
+
if (totalToolCalls === 0) return null;
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
metric: {
|
|
109
|
+
ratio: gatheringCalls / totalToolCalls,
|
|
110
|
+
gatheringCalls,
|
|
111
|
+
actionCalls,
|
|
112
|
+
totalToolCalls,
|
|
113
|
+
leadingGather,
|
|
114
|
+
turns,
|
|
115
|
+
},
|
|
116
|
+
outcome: { userPrompts, toolErrors },
|
|
117
|
+
};
|
|
118
|
+
}
|