world-model-optimizer 0.2.0__py3-none-any.whl
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.
- llm_waterfall/LICENSE +21 -0
- llm_waterfall/__init__.py +53 -0
- llm_waterfall/adapters/__init__.py +36 -0
- llm_waterfall/adapters/anthropic.py +105 -0
- llm_waterfall/adapters/aws_mantle.py +47 -0
- llm_waterfall/adapters/azure_openai.py +71 -0
- llm_waterfall/adapters/base.py +51 -0
- llm_waterfall/adapters/bedrock.py +309 -0
- llm_waterfall/adapters/openai.py +130 -0
- llm_waterfall/classify.py +184 -0
- llm_waterfall/pricing.py +110 -0
- llm_waterfall/py.typed +0 -0
- llm_waterfall/types.py +295 -0
- llm_waterfall/waterfall.py +255 -0
- wmo/__init__.py +38 -0
- wmo/agents/__init__.py +7 -0
- wmo/agents/default.py +29 -0
- wmo/agents/meta.py +55 -0
- wmo/agents/optimizer.py +55 -0
- wmo/agents/project.py +928 -0
- wmo/cli/__init__.py +5 -0
- wmo/cli/agent_session.py +1123 -0
- wmo/cli/app.py +2489 -0
- wmo/cli/e2b_cmds.py +212 -0
- wmo/cli/eval_closed_loop.py +207 -0
- wmo/cli/harness_app.py +1147 -0
- wmo/cli/harness_distill.py +659 -0
- wmo/cli/hosted_session.py +880 -0
- wmo/cli/ingest_cmd.py +165 -0
- wmo/cli/model_roles.py +82 -0
- wmo/cli/platform_cmds.py +372 -0
- wmo/cli/route_app.py +274 -0
- wmo/cli/session_state.py +243 -0
- wmo/cli/ui.py +1107 -0
- wmo/cli/workspace_sync.py +504 -0
- wmo/config/__init__.py +60 -0
- wmo/config/card.py +129 -0
- wmo/config/config.py +367 -0
- wmo/config/dotenv.py +67 -0
- wmo/config/settings.py +128 -0
- wmo/config/store.py +177 -0
- wmo/conftest.py +19 -0
- wmo/connect/__init__.py +88 -0
- wmo/connect/apps.py +78 -0
- wmo/connect/brave.py +284 -0
- wmo/connect/connector.py +79 -0
- wmo/connect/credentials.py +164 -0
- wmo/connect/github.py +321 -0
- wmo/connect/google.py +627 -0
- wmo/connect/notion.py +790 -0
- wmo/connect/oauth.py +461 -0
- wmo/connect/slack.py +555 -0
- wmo/connect/store.py +199 -0
- wmo/connect/types.py +156 -0
- wmo/core/__init__.py +21 -0
- wmo/core/parsing.py +281 -0
- wmo/core/render.py +271 -0
- wmo/core/text.py +40 -0
- wmo/core/types.py +116 -0
- wmo/distill/__init__.py +14 -0
- wmo/distill/agents.py +140 -0
- wmo/distill/config.py +1006 -0
- wmo/distill/cost.py +437 -0
- wmo/distill/data.py +921 -0
- wmo/distill/deadlines.py +254 -0
- wmo/distill/fake_tinker.py +734 -0
- wmo/distill/gate.py +122 -0
- wmo/distill/loop.py +3499 -0
- wmo/distill/renderers.py +399 -0
- wmo/distill/rendering.py +620 -0
- wmo/distill/rollouts.py +726 -0
- wmo/distill/samples.py +195 -0
- wmo/distill/store.py +829 -0
- wmo/distill/teacher.py +714 -0
- wmo/distill/tokens.py +535 -0
- wmo/distill/tracking.py +552 -0
- wmo/distill/tripwire.py +411 -0
- wmo/distill/xtoken/byte_offsets.py +152 -0
- wmo/distill/xtoken/chunks.py +457 -0
- wmo/distill/xtoken/prompt_logprobs.py +475 -0
- wmo/distill/xtoken/teacher_render.py +346 -0
- wmo/engine/__init__.py +28 -0
- wmo/engine/autoconfig.py +367 -0
- wmo/engine/build.py +346 -0
- wmo/engine/demo.py +77 -0
- wmo/engine/eval_suites.py +245 -0
- wmo/engine/grounding.py +491 -0
- wmo/engine/knowledge.py +291 -0
- wmo/engine/loader.py +36 -0
- wmo/engine/play.py +92 -0
- wmo/engine/prompts.py +99 -0
- wmo/engine/replay.py +443 -0
- wmo/engine/reporting.py +58 -0
- wmo/engine/workspace.py +468 -0
- wmo/engine/world_model.py +568 -0
- wmo/env/__init__.py +22 -0
- wmo/env/base.py +121 -0
- wmo/env/closed_loop.py +229 -0
- wmo/env/episode.py +107 -0
- wmo/env/llm_agent.py +93 -0
- wmo/env/scenarios.py +73 -0
- wmo/evals/__init__.py +52 -0
- wmo/evals/agreement.py +110 -0
- wmo/evals/base.py +45 -0
- wmo/evals/closed_loop.py +480 -0
- wmo/evals/failover.py +96 -0
- wmo/evals/gold.py +127 -0
- wmo/evals/grid.py +394 -0
- wmo/evals/grid_plot.py +205 -0
- wmo/evals/harbor/__init__.py +27 -0
- wmo/evals/harbor/agent.py +573 -0
- wmo/evals/harbor/ctrf.py +171 -0
- wmo/evals/harbor/e2b_environment.py +587 -0
- wmo/evals/harbor/e2b_template_policy.py +144 -0
- wmo/evals/harbor/scorer.py +875 -0
- wmo/evals/harbor/tasks.py +140 -0
- wmo/evals/open_loop.py +194 -0
- wmo/evals/tasks.py +53 -0
- wmo/harness/__init__.py +51 -0
- wmo/harness/code_runtime.py +288 -0
- wmo/harness/create.py +1191 -0
- wmo/harness/delta.py +220 -0
- wmo/harness/doc.py +556 -0
- wmo/harness/e2b_ledger.py +342 -0
- wmo/harness/e2b_reap.py +476 -0
- wmo/harness/e2b_sandbox.py +350 -0
- wmo/harness/environment.py +35 -0
- wmo/harness/live_session.py +543 -0
- wmo/harness/mutate.py +343 -0
- wmo/harness/pi_e2b.py +1710 -0
- wmo/harness/pi_entry/entry.ts +268 -0
- wmo/harness/pi_entry/runner_frames.ts +92 -0
- wmo/harness/pi_entry/runner_live.ts +587 -0
- wmo/harness/pi_entry/runner_service.ts +270 -0
- wmo/harness/pi_entry/runner_stdio.ts +374 -0
- wmo/harness/pi_entry/runner_termination.ts +142 -0
- wmo/harness/pi_local.py +262 -0
- wmo/harness/pi_runtime.py +495 -0
- wmo/harness/pi_vendor.py +65 -0
- wmo/harness/population.py +509 -0
- wmo/harness/project_proposer.py +569 -0
- wmo/harness/proposer.py +977 -0
- wmo/harness/runner_link.py +619 -0
- wmo/harness/runtime.py +389 -0
- wmo/harness/scoring.py +247 -0
- wmo/harness/skills.py +116 -0
- wmo/harness/source_tree.py +319 -0
- wmo/harness/store.py +176 -0
- wmo/harness/tools.py +105 -0
- wmo/harness/vendor/manifest.sha256 +58 -0
- wmo/harness/vendor/pi-agent/CHANGELOG.md +556 -0
- wmo/harness/vendor/pi-agent/LICENSE +21 -0
- wmo/harness/vendor/pi-agent/README.md +488 -0
- wmo/harness/vendor/pi-agent/VENDOR.md +39 -0
- wmo/harness/vendor/pi-agent/docs/agent-harness.md +486 -0
- wmo/harness/vendor/pi-agent/docs/durable-harness.md +212 -0
- wmo/harness/vendor/pi-agent/docs/hooks.md +445 -0
- wmo/harness/vendor/pi-agent/docs/models.md +966 -0
- wmo/harness/vendor/pi-agent/docs/observability.md +376 -0
- wmo/harness/vendor/pi-agent/package.json +60 -0
- wmo/harness/vendor/pi-agent/src/agent-loop.ts +748 -0
- wmo/harness/vendor/pi-agent/src/agent.ts +575 -0
- wmo/harness/vendor/pi-agent/src/harness/agent-harness.ts +1029 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/branch-summarization.ts +261 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/compaction.ts +747 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/utils.ts +144 -0
- wmo/harness/vendor/pi-agent/src/harness/env/nodejs.ts +550 -0
- wmo/harness/vendor/pi-agent/src/harness/messages.ts +164 -0
- wmo/harness/vendor/pi-agent/src/harness/prompt-templates.ts +267 -0
- wmo/harness/vendor/pi-agent/src/harness/session/jsonl-repo.ts +177 -0
- wmo/harness/vendor/pi-agent/src/harness/session/jsonl-storage.ts +293 -0
- wmo/harness/vendor/pi-agent/src/harness/session/memory-repo.ts +50 -0
- wmo/harness/vendor/pi-agent/src/harness/session/memory-storage.ts +131 -0
- wmo/harness/vendor/pi-agent/src/harness/session/repo-utils.ts +51 -0
- wmo/harness/vendor/pi-agent/src/harness/session/session.ts +267 -0
- wmo/harness/vendor/pi-agent/src/harness/session/uuid.ts +54 -0
- wmo/harness/vendor/pi-agent/src/harness/skills.ts +375 -0
- wmo/harness/vendor/pi-agent/src/harness/system-prompt.ts +34 -0
- wmo/harness/vendor/pi-agent/src/harness/types.ts +836 -0
- wmo/harness/vendor/pi-agent/src/harness/utils/shell-output.ts +135 -0
- wmo/harness/vendor/pi-agent/src/harness/utils/truncate.ts +344 -0
- wmo/harness/vendor/pi-agent/src/index.ts +44 -0
- wmo/harness/vendor/pi-agent/src/node.ts +2 -0
- wmo/harness/vendor/pi-agent/src/proxy.ts +367 -0
- wmo/harness/vendor/pi-agent/src/types.ts +428 -0
- wmo/harness/vendor/pi-agent/test/agent-loop.test.ts +1351 -0
- wmo/harness/vendor/pi-agent/test/agent.test.ts +699 -0
- wmo/harness/vendor/pi-agent/test/e2e.test.ts +404 -0
- wmo/harness/vendor/pi-agent/test/harness/agent-harness-stream.test.ts +213 -0
- wmo/harness/vendor/pi-agent/test/harness/agent-harness.test.ts +608 -0
- wmo/harness/vendor/pi-agent/test/harness/compaction.test.ts +655 -0
- wmo/harness/vendor/pi-agent/test/harness/nodejs-env.test.ts +321 -0
- wmo/harness/vendor/pi-agent/test/harness/prompt-templates.test.ts +90 -0
- wmo/harness/vendor/pi-agent/test/harness/repo.test.ts +68 -0
- wmo/harness/vendor/pi-agent/test/harness/resource-formatting.test.ts +24 -0
- wmo/harness/vendor/pi-agent/test/harness/session-test-utils.ts +55 -0
- wmo/harness/vendor/pi-agent/test/harness/session-uuid.test.ts +50 -0
- wmo/harness/vendor/pi-agent/test/harness/session.test.ts +156 -0
- wmo/harness/vendor/pi-agent/test/harness/skills.test.ts +116 -0
- wmo/harness/vendor/pi-agent/test/harness/storage.test.ts +299 -0
- wmo/harness/vendor/pi-agent/test/harness/system-prompt.test.ts +66 -0
- wmo/harness/vendor/pi-agent/test/harness/truncate.test.ts +169 -0
- wmo/harness/vendor/pi-agent/test/scratch/simple.ts +72 -0
- wmo/harness/vendor/pi-agent/test/utils/calculate.ts +32 -0
- wmo/harness/vendor/pi-agent/test/utils/get-current-time.ts +46 -0
- wmo/harness/vendor/pi-agent/tsconfig.build.json +13 -0
- wmo/harness/vendor/pi-agent/vitest.config.ts +19 -0
- wmo/harness/vendor/pi-agent/vitest.harness.config.ts +28 -0
- wmo/harness/vendor/vendor_pi.sh +59 -0
- wmo/harness/workspace_patch.py +270 -0
- wmo/ingest/__init__.py +47 -0
- wmo/ingest/adapter.py +72 -0
- wmo/ingest/base.py +114 -0
- wmo/ingest/braintrust.py +339 -0
- wmo/ingest/detect.py +126 -0
- wmo/ingest/langfuse.py +291 -0
- wmo/ingest/langsmith.py +444 -0
- wmo/ingest/mastra.py +330 -0
- wmo/ingest/messages.py +170 -0
- wmo/ingest/normalize.py +679 -0
- wmo/ingest/otel_genai.py +69 -0
- wmo/ingest/otel_writer.py +100 -0
- wmo/ingest/phoenix.py +150 -0
- wmo/ingest/postgres.py +246 -0
- wmo/ingest/posthog.py +320 -0
- wmo/ingest/quality.py +28 -0
- wmo/ingest/stream.py +209 -0
- wmo/ingest/testdata/sample_otlp.json +60 -0
- wmo/ingest/testdata/sample_spans.jsonl +3 -0
- wmo/optimize/__init__.py +25 -0
- wmo/optimize/base.py +143 -0
- wmo/optimize/gepa.py +806 -0
- wmo/optimize/judge.py +262 -0
- wmo/optimize/judge_quality.py +359 -0
- wmo/optimize/knn.py +468 -0
- wmo/optimize/numeric.py +152 -0
- wmo/optimize/outcomes.py +103 -0
- wmo/optimize/policy.py +669 -0
- wmo/optimize/report.py +231 -0
- wmo/optimize/reward.py +129 -0
- wmo/optimize/routing.py +373 -0
- wmo/platform/__init__.py +6 -0
- wmo/platform/auth.py +115 -0
- wmo/platform/client.py +551 -0
- wmo/platform/credentials.py +126 -0
- wmo/platform/transfer.py +158 -0
- wmo/providers/__init__.py +40 -0
- wmo/providers/_bedrock_chat.py +155 -0
- wmo/providers/_openai_common.py +182 -0
- wmo/providers/_responses_common.py +472 -0
- wmo/providers/anthropic.py +134 -0
- wmo/providers/azure_openai.py +296 -0
- wmo/providers/base.py +300 -0
- wmo/providers/bedrock.py +312 -0
- wmo/providers/models.py +205 -0
- wmo/providers/openai.py +143 -0
- wmo/providers/openai_responses.py +240 -0
- wmo/providers/pool.py +170 -0
- wmo/providers/registry.py +73 -0
- wmo/providers/retry.py +151 -0
- wmo/providers/tinker.py +936 -0
- wmo/providers/waterfall.py +336 -0
- wmo/research/__init__.py +81 -0
- wmo/research/ablation.py +133 -0
- wmo/research/concurrency_plot.py +523 -0
- wmo/research/concurrency_run.py +240 -0
- wmo/research/concurrency_scaling.py +270 -0
- wmo/research/gepa_scaling.py +274 -0
- wmo/research/pipeline.py +198 -0
- wmo/research/scaling_split.py +82 -0
- wmo/research/scenario_fidelity.py +198 -0
- wmo/research/scenario_recovery.py +92 -0
- wmo/research/seed_stability.py +90 -0
- wmo/research/trace_scaling.py +348 -0
- wmo/retrieval/__init__.py +6 -0
- wmo/retrieval/embedders.py +105 -0
- wmo/retrieval/leakfree.py +52 -0
- wmo/retrieval/retriever.py +173 -0
- wmo/scenarios/__init__.py +58 -0
- wmo/scenarios/builder.py +152 -0
- wmo/scenarios/mining/__init__.py +27 -0
- wmo/scenarios/mining/clustering.py +171 -0
- wmo/scenarios/mining/facets.py +226 -0
- wmo/scenarios/mining/selection.py +220 -0
- wmo/scenarios/synthesis/__init__.py +6 -0
- wmo/scenarios/synthesis/scenario_set.py +63 -0
- wmo/scenarios/synthesis/synthesizer.py +85 -0
- wmo/scenarios/verification/__init__.py +17 -0
- wmo/scenarios/verification/judge.py +97 -0
- wmo/scenarios/verification/verify.py +135 -0
- wmo/serving/__init__.py +5 -0
- wmo/serving/builds.py +451 -0
- wmo/serving/chat.py +878 -0
- wmo/serving/endpoint_config.py +64 -0
- wmo/serving/savings.py +250 -0
- wmo/serving/server.py +553 -0
- wmo/serving/traces_source.py +206 -0
- wmo/telemetry.py +213 -0
- wmo/tracking/__init__.py +36 -0
- wmo/tracking/clock.py +24 -0
- wmo/tracking/metered.py +125 -0
- wmo/tracking/pricing.py +99 -0
- wmo/tracking/store.py +31 -0
- wmo/tracking/tracker.py +149 -0
- world_model_optimizer-0.2.0.dist-info/METADATA +203 -0
- world_model_optimizer-0.2.0.dist-info/RECORD +308 -0
- world_model_optimizer-0.2.0.dist-info/WHEEL +4 -0
- world_model_optimizer-0.2.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live-session pi runner: the in-sandbox peer of wmo/harness/live_session.py (LiveSession).
|
|
3
|
+
*
|
|
4
|
+
* Where runner_stdio.ts runs ONE fire-and-forget episode, this runner hosts ONE long-lived pi
|
|
5
|
+
* Agent for an interactive multi-turn session: the host sends `session_start` once (materializing
|
|
6
|
+
* the champion's code surfaces), then `user_message` / `abort` / `ping` frames over the session's
|
|
7
|
+
* life. Interactive sessions retain one transcript; filesystem-backed projects can instead scope
|
|
8
|
+
* conversation to each outer turn while reusing the same Agent, runner, and project filesystem.
|
|
9
|
+
* The transport, the localhost LLM bridge (credentials stay host-side), and the env-tools-as-
|
|
10
|
+
* `tool_request` contract are identical to runner_stdio.ts — deliberately re-mirrored rather than
|
|
11
|
+
* imported, because runner_stdio boots per-episode helpers and this runner's lifecycle differs.
|
|
12
|
+
*
|
|
13
|
+
* stdout is the frame stream and NOTHING else may write to it: console.* is rebound to stderr
|
|
14
|
+
* before any agent code loads. One base64(JSON) frame per line.
|
|
15
|
+
*
|
|
16
|
+
* Run (inside the sandbox, workdir holding node_modules + package.json {"type":"module"}):
|
|
17
|
+
* cd /home/user/pi-run && node --experimental-strip-types runner_live.ts
|
|
18
|
+
*/
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import http from "node:http";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import util from "node:util";
|
|
23
|
+
import { pathToFileURL } from "node:url";
|
|
24
|
+
import type { AddressInfo } from "node:net";
|
|
25
|
+
|
|
26
|
+
// CRITICAL — FIRST statements: rebind every stdout-defaulting console channel to stderr before
|
|
27
|
+
// anything else runs, so stray prints can never corrupt the frame stream.
|
|
28
|
+
const toStderr = (...args: unknown[]): void => {
|
|
29
|
+
process.stderr.write(util.format(...args) + "\n");
|
|
30
|
+
};
|
|
31
|
+
console.log = toStderr;
|
|
32
|
+
console.info = toStderr;
|
|
33
|
+
console.warn = toStderr;
|
|
34
|
+
console.debug = toStderr;
|
|
35
|
+
|
|
36
|
+
const AGENT_MODEL = process.env.PI_AGENT_MODEL ?? "worker";
|
|
37
|
+
// Last-resort model context window when session_start carries none. The host resolves the REAL
|
|
38
|
+
// served window (provider/SDK model info) and sends it as context_window; never assume a size here.
|
|
39
|
+
const DEFAULT_CONTEXT_WINDOW = 128000;
|
|
40
|
+
const TRANSPORT_KEEPALIVE_MS = 30_000;
|
|
41
|
+
const LIVE_OUTBOX = process.env.WMO_LIVE_OUTBOX?.trim() || null;
|
|
42
|
+
|
|
43
|
+
type Frame = Record<string, any>;
|
|
44
|
+
type TransportEnvelope = { transport_seq: number; frame: Frame };
|
|
45
|
+
|
|
46
|
+
function encodeFrame(frame: Frame): string {
|
|
47
|
+
return Buffer.from(JSON.stringify(frame), "utf8").toString("base64") + "\n";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function decodeFrame(line: string): Frame | null {
|
|
51
|
+
const text = line.trim();
|
|
52
|
+
if (!text) return null;
|
|
53
|
+
try {
|
|
54
|
+
const frame = JSON.parse(Buffer.from(text, "base64").toString("utf8"));
|
|
55
|
+
return frame && typeof frame === "object" && !Array.isArray(frame) ? (frame as Frame) : null;
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Durable, replayable copy of the semantic output stream.
|
|
63
|
+
*
|
|
64
|
+
* A frame file is published before the head watermark advances, and both writes use a temporary
|
|
65
|
+
* sibling plus rename. Readers may therefore trust every sequence through `head` without ever
|
|
66
|
+
* observing partial JSON. The decimal watermark also lets a replacement runner continue a
|
|
67
|
+
* pre-existing outbox without reusing committed sequence numbers.
|
|
68
|
+
*/
|
|
69
|
+
class DurableOutbox {
|
|
70
|
+
private readonly framesDir: string;
|
|
71
|
+
private readonly headPath: string;
|
|
72
|
+
private readonly tmpNamespace = `${process.pid}-${Date.now().toString(36)}-${process.hrtime.bigint().toString(36)}`;
|
|
73
|
+
private transportSeq = 0;
|
|
74
|
+
private tmpSeq = 0;
|
|
75
|
+
|
|
76
|
+
constructor(root: string) {
|
|
77
|
+
const resolvedRoot = path.resolve(root);
|
|
78
|
+
this.framesDir = path.join(resolvedRoot, "frames");
|
|
79
|
+
this.headPath = path.join(resolvedRoot, "head");
|
|
80
|
+
fs.mkdirSync(this.framesDir, { recursive: true });
|
|
81
|
+
|
|
82
|
+
if (fs.existsSync(this.headPath)) {
|
|
83
|
+
const current = fs.readFileSync(this.headPath, "utf8").trim();
|
|
84
|
+
const parsed = Number(current);
|
|
85
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) {
|
|
86
|
+
throw new Error(`invalid live outbox head at ${this.headPath}: ${JSON.stringify(current)}`);
|
|
87
|
+
}
|
|
88
|
+
this.transportSeq = parsed;
|
|
89
|
+
} else {
|
|
90
|
+
this.atomicWrite(this.headPath, "0\n");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
publish(frame: Frame): TransportEnvelope {
|
|
95
|
+
const transport_seq = this.transportSeq + 1;
|
|
96
|
+
const envelope: TransportEnvelope = { transport_seq, frame };
|
|
97
|
+
const filename = `${String(transport_seq).padStart(20, "0")}.json`;
|
|
98
|
+
this.atomicWrite(path.join(this.framesDir, filename), JSON.stringify(envelope) + "\n");
|
|
99
|
+
this.atomicWrite(this.headPath, `${transport_seq}\n`);
|
|
100
|
+
this.transportSeq = transport_seq;
|
|
101
|
+
return envelope;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private atomicWrite(target: string, content: string): void {
|
|
105
|
+
const tmp = path.join(
|
|
106
|
+
path.dirname(target),
|
|
107
|
+
`.${path.basename(target)}.tmp-${this.tmpNamespace}-${++this.tmpSeq}`,
|
|
108
|
+
);
|
|
109
|
+
try {
|
|
110
|
+
fs.writeFileSync(tmp, content, { encoding: "utf8", flag: "wx" });
|
|
111
|
+
fs.renameSync(tmp, target);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
try {
|
|
114
|
+
fs.unlinkSync(tmp);
|
|
115
|
+
} catch {
|
|
116
|
+
// The temp file may not have been created, or rename may already have consumed it.
|
|
117
|
+
}
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The stdio twin of runner_frames.FrameConn: `request` awaits a matching response by req_id;
|
|
124
|
+
* host-pushed frames (session_start, user_message, abort, ping, shutdown) fire handlers. */
|
|
125
|
+
class StdioConn {
|
|
126
|
+
private buf = "";
|
|
127
|
+
private waiters = new Map<number, (f: Frame) => void>();
|
|
128
|
+
private handlers = new Map<string, (f: Frame) => void>();
|
|
129
|
+
private reqSeq = 0;
|
|
130
|
+
private readonly outbox: DurableOutbox | null;
|
|
131
|
+
private lastInboundSeq = 0;
|
|
132
|
+
|
|
133
|
+
constructor() {
|
|
134
|
+
// Initialize the complete durable layout before `main` can emit its ready/hello frame.
|
|
135
|
+
this.outbox = LIVE_OUTBOX ? new DurableOutbox(LIVE_OUTBOX) : null;
|
|
136
|
+
process.stdin.setEncoding("utf8");
|
|
137
|
+
process.stdin.on("data", (chunk: string) => this.onData(chunk));
|
|
138
|
+
process.stdin.on("end", () => process.exit(0));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
on(type: string, handler: (f: Frame) => void): void {
|
|
142
|
+
this.handlers.set(type, handler);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
send(frame: Frame): void {
|
|
146
|
+
if (!this.outbox) {
|
|
147
|
+
// Keep the original wire format byte-for-byte when durable transport is not requested.
|
|
148
|
+
process.stdout.write(encodeFrame(frame));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const envelope = this.outbox.publish(frame);
|
|
152
|
+
process.stdout.write(encodeFrame(envelope));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
request(type: string, payload: Frame): Promise<Frame> {
|
|
156
|
+
const req_id = ++this.reqSeq;
|
|
157
|
+
return new Promise((resolve) => {
|
|
158
|
+
this.waiters.set(req_id, resolve);
|
|
159
|
+
this.send({ type, req_id, ...payload });
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Keep the command stream active across the persistent session; timeout remains host-owned. */
|
|
164
|
+
startTransportKeepalive(): () => void {
|
|
165
|
+
const timer = setInterval(
|
|
166
|
+
// Liveness ticks carry no semantic state: leave them on the legacy wire and out of the
|
|
167
|
+
// replay log so they neither advance the sequence nor create unbounded tiny files.
|
|
168
|
+
() => process.stdout.write(encodeFrame({ type: "transport_keepalive" })),
|
|
169
|
+
TRANSPORT_KEEPALIVE_MS,
|
|
170
|
+
);
|
|
171
|
+
timer.unref();
|
|
172
|
+
return () => clearInterval(timer);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private onData(chunk: string): void {
|
|
176
|
+
this.buf += chunk;
|
|
177
|
+
let nl = this.buf.indexOf("\n");
|
|
178
|
+
while (nl >= 0) {
|
|
179
|
+
const line = this.buf.slice(0, nl);
|
|
180
|
+
this.buf = this.buf.slice(nl + 1);
|
|
181
|
+
const frame = decodeFrame(line);
|
|
182
|
+
if (frame) this.dispatchInbound(frame);
|
|
183
|
+
nl = this.buf.indexOf("\n");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private dispatchInbound(value: Frame): void {
|
|
188
|
+
if (!this.outbox) {
|
|
189
|
+
this.dispatch(value);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const inboundSeq = value.transport_in_seq;
|
|
194
|
+
const frame = value.frame;
|
|
195
|
+
if (
|
|
196
|
+
typeof inboundSeq !== "number" ||
|
|
197
|
+
!Number.isSafeInteger(inboundSeq) ||
|
|
198
|
+
inboundSeq <= 0 ||
|
|
199
|
+
!frame ||
|
|
200
|
+
typeof frame !== "object" ||
|
|
201
|
+
Array.isArray(frame)
|
|
202
|
+
) {
|
|
203
|
+
this.send({
|
|
204
|
+
type: "transport_nack",
|
|
205
|
+
transport_in_seq: inboundSeq,
|
|
206
|
+
expected_transport_in_seq: this.lastInboundSeq + 1,
|
|
207
|
+
});
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (inboundSeq <= this.lastInboundSeq) {
|
|
211
|
+
// The original dispatch succeeded but its ack notification was lost. Re-publish only
|
|
212
|
+
// the acknowledgement: repeating a response, prompt, or tool result is unsafe.
|
|
213
|
+
this.send({ type: "transport_ack", transport_in_seq: inboundSeq });
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (inboundSeq !== this.lastInboundSeq + 1) {
|
|
217
|
+
this.send({
|
|
218
|
+
type: "transport_nack",
|
|
219
|
+
transport_in_seq: inboundSeq,
|
|
220
|
+
expected_transport_in_seq: this.lastInboundSeq + 1,
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
this.lastInboundSeq = inboundSeq;
|
|
226
|
+
if (frame.type === "shutdown") {
|
|
227
|
+
// shutdown exits synchronously, so persist acceptance before dispatching it.
|
|
228
|
+
this.send({ type: "transport_ack", transport_in_seq: inboundSeq });
|
|
229
|
+
this.dispatch(frame);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
this.dispatch(frame);
|
|
233
|
+
this.send({ type: "transport_ack", transport_in_seq: inboundSeq });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private dispatch(frame: Frame): void {
|
|
237
|
+
const rid = frame.req_id;
|
|
238
|
+
if (typeof rid === "number" && this.waiters.has(rid)) {
|
|
239
|
+
const resolve = this.waiters.get(rid);
|
|
240
|
+
this.waiters.delete(rid);
|
|
241
|
+
resolve?.(frame);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const handler = this.handlers.get(frame.type);
|
|
245
|
+
if (handler) handler(frame);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function assistantText(msg: any): string {
|
|
250
|
+
if (!msg || msg.role !== "assistant" || !Array.isArray(msg.content)) return "";
|
|
251
|
+
return msg.content
|
|
252
|
+
.filter((c: any) => c?.type === "text")
|
|
253
|
+
.map((c: any) => String(c.text ?? ""))
|
|
254
|
+
.join("")
|
|
255
|
+
.trim();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
interface Bridge {
|
|
259
|
+
url: string;
|
|
260
|
+
close: () => void;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Localhost endpoint pi's openai-completions transport POSTs to; frames each request to the host
|
|
264
|
+
* and renders the returned completion back as the SSE pi's parser expects. Creds stay host-side. */
|
|
265
|
+
function startLlmBridge(conn: StdioConn): Promise<Bridge> {
|
|
266
|
+
return new Promise((resolve) => {
|
|
267
|
+
const server = http.createServer((req, res) => {
|
|
268
|
+
const chunks: Buffer[] = [];
|
|
269
|
+
req.on("data", (c: Buffer) => chunks.push(c));
|
|
270
|
+
req.on("end", async () => {
|
|
271
|
+
res.writeHead(200, { "Content-Type": "text/event-stream", Connection: "close" });
|
|
272
|
+
try {
|
|
273
|
+
const body = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
|
|
274
|
+
const reply = await conn.request("llm_request", { openai_body: body });
|
|
275
|
+
if (reply.error) {
|
|
276
|
+
res.end(`data: ${JSON.stringify({ error: { message: reply.error } })}\n\ndata: [DONE]\n\n`);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const choice = reply.completion?.choices?.[0] ?? {};
|
|
280
|
+
const msg = choice.message ?? {};
|
|
281
|
+
const delta: any = { role: "assistant", content: msg.content ?? "" };
|
|
282
|
+
if (msg.reasoning_details) delta.reasoning_details = msg.reasoning_details;
|
|
283
|
+
if (msg.tool_calls) {
|
|
284
|
+
delta.tool_calls = msg.tool_calls.map((tc: any, i: number) => ({
|
|
285
|
+
index: i,
|
|
286
|
+
id: tc.id,
|
|
287
|
+
type: tc.type ?? "function",
|
|
288
|
+
function: tc.function ?? {},
|
|
289
|
+
}));
|
|
290
|
+
}
|
|
291
|
+
const first = { choices: [{ index: 0, delta, finish_reason: null }] };
|
|
292
|
+
const last = {
|
|
293
|
+
choices: [{ index: 0, delta: {}, finish_reason: choice.finish_reason ?? "stop" }],
|
|
294
|
+
// Pi uses the latest assistant usage to estimate occupied context. Without this,
|
|
295
|
+
// it falls back to chars/4 and can prematurely clamp the next output budget.
|
|
296
|
+
usage: reply.completion?.usage,
|
|
297
|
+
};
|
|
298
|
+
res.write(`data: ${JSON.stringify(first)}\n\n`);
|
|
299
|
+
res.write(`data: ${JSON.stringify(last)}\n\n`);
|
|
300
|
+
res.end("data: [DONE]\n\n");
|
|
301
|
+
} catch (e) {
|
|
302
|
+
res.end(`data: ${JSON.stringify({ error: { message: String(e) } })}\n\ndata: [DONE]\n\n`);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
server.listen(0, "127.0.0.1", () => {
|
|
307
|
+
const addr = server.address() as AddressInfo;
|
|
308
|
+
resolve({ url: `http://127.0.0.1:${addr.port}/v1`, close: () => server.close() });
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Materialize session_start.files (the champion's code surfaces) into ./live/ once and import the
|
|
314
|
+
* Agent. Unlike the episode runner there is no per-run dir — the session reuses one Agent. */
|
|
315
|
+
async function loadAgent(start: Frame): Promise<any> {
|
|
316
|
+
const files: Record<string, string> = start.files ?? {};
|
|
317
|
+
if (!files["src/agent.ts"]) {
|
|
318
|
+
throw new Error("session_start carried no src/agent.ts (the live runner has no static fallback)");
|
|
319
|
+
}
|
|
320
|
+
const base = path.join(process.cwd(), "live");
|
|
321
|
+
const basePrefix = base.endsWith(path.sep) ? base : base + path.sep;
|
|
322
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
323
|
+
// Keep every materialized file under ./live: `path.join`/`resolve` let a
|
|
324
|
+
// `../` or absolute manifest key escape and overwrite arbitrary sandbox
|
|
325
|
+
// files, so reject anything that resolves outside the base.
|
|
326
|
+
const dst = path.resolve(base, rel);
|
|
327
|
+
if (dst !== base && !dst.startsWith(basePrefix)) {
|
|
328
|
+
throw new Error(`session_start file path escapes the live directory: ${rel}`);
|
|
329
|
+
}
|
|
330
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
331
|
+
fs.writeFileSync(dst, content);
|
|
332
|
+
}
|
|
333
|
+
const mod = await import(pathToFileURL(path.join(base, "src/agent.ts")).href);
|
|
334
|
+
if (typeof mod.Agent !== "function") {
|
|
335
|
+
throw new Error("champion src/agent.ts does not export an Agent class");
|
|
336
|
+
}
|
|
337
|
+
return mod.Agent;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const REQUIRED_AGENT_METHODS = ["prompt", "steer", "abort", "subscribe"] as const;
|
|
341
|
+
|
|
342
|
+
function assertInteractive(AgentCtor: any): void {
|
|
343
|
+
for (const method of REQUIRED_AGENT_METHODS) {
|
|
344
|
+
if (typeof AgentCtor.prototype?.[method] !== "function") {
|
|
345
|
+
throw new Error(
|
|
346
|
+
`champion Agent is not steerable: missing ${method}() — this harness cannot run a live session`,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function userMessage(text: string): any {
|
|
353
|
+
return { role: "user", content: [{ type: "text", text }], timestamp: Date.now() };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* After an aborted run, the transcript tail can hold an assistant message whose toolCalls never got
|
|
358
|
+
* results (the loop returned on the abort signal before executing/finishing them). Left as-is, the
|
|
359
|
+
* NEXT provider request is a 400 (OpenAI) / validation error (Bedrock Converse): an assistant
|
|
360
|
+
* tool-call with no matching tool result. Append a synthetic "cancelled by user" result for every
|
|
361
|
+
* orphaned toolCall so the next turn is well-formed. (Vendored eval runners never hit this — they
|
|
362
|
+
* only ever run one prompt; interactivity is what surfaces it.)
|
|
363
|
+
*/
|
|
364
|
+
function repairOrphanedToolCalls(agent: any): void {
|
|
365
|
+
const messages: any[] = agent.state?.messages ?? [];
|
|
366
|
+
const resolved = new Set<string>();
|
|
367
|
+
for (const m of messages) {
|
|
368
|
+
if (m?.role === "toolResult" && typeof m.toolCallId === "string") resolved.add(m.toolCallId);
|
|
369
|
+
}
|
|
370
|
+
const repairs: any[] = [];
|
|
371
|
+
for (const m of messages) {
|
|
372
|
+
if (m?.role !== "assistant" || !Array.isArray(m.content)) continue;
|
|
373
|
+
for (const block of m.content) {
|
|
374
|
+
if (block?.type === "toolCall" && typeof block.id === "string" && !resolved.has(block.id)) {
|
|
375
|
+
resolved.add(block.id);
|
|
376
|
+
repairs.push({
|
|
377
|
+
role: "toolResult",
|
|
378
|
+
toolCallId: block.id,
|
|
379
|
+
toolName: block.name ?? "",
|
|
380
|
+
content: [{ type: "text", text: "cancelled by user" }],
|
|
381
|
+
details: {},
|
|
382
|
+
isError: true,
|
|
383
|
+
timestamp: Date.now(),
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (repairs.length > 0) agent.state.messages = [...messages, ...repairs];
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Owns the single Agent and serializes prompt/steer/abort across the session's frames. */
|
|
392
|
+
class Session {
|
|
393
|
+
// NOTE: fields declared explicitly (not via constructor parameter properties) — node's
|
|
394
|
+
// --experimental-strip-types rejects `constructor(private x)` in strip-only mode.
|
|
395
|
+
private readonly conn: StdioConn;
|
|
396
|
+
private agent: any = null;
|
|
397
|
+
private bridge: Bridge | null = null;
|
|
398
|
+
private running = false;
|
|
399
|
+
private turns = 0;
|
|
400
|
+
private turnCap = 60;
|
|
401
|
+
private conversationScope: "session" | "turn" = "session";
|
|
402
|
+
private interrupted = false;
|
|
403
|
+
private hitTurnCap = false;
|
|
404
|
+
|
|
405
|
+
constructor(conn: StdioConn) {
|
|
406
|
+
this.conn = conn;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async start(frame: Frame): Promise<void> {
|
|
410
|
+
this.turnCap = Number(frame.turn_cap ?? 60);
|
|
411
|
+
this.conversationScope = frame.conversation_scope === "turn" ? "turn" : "session";
|
|
412
|
+
const maxOutputTokens =
|
|
413
|
+
Number.isInteger(frame.max_output_tokens) && frame.max_output_tokens >= 1
|
|
414
|
+
? frame.max_output_tokens
|
|
415
|
+
: 4096;
|
|
416
|
+
const contextWindow =
|
|
417
|
+
Number.isInteger(frame.context_window) && frame.context_window >= 1024
|
|
418
|
+
? frame.context_window
|
|
419
|
+
: DEFAULT_CONTEXT_WINDOW;
|
|
420
|
+
const AgentCtor = await loadAgent(frame);
|
|
421
|
+
assertInteractive(AgentCtor);
|
|
422
|
+
this.bridge = await startLlmBridge(this.conn);
|
|
423
|
+
|
|
424
|
+
const model = {
|
|
425
|
+
id: AGENT_MODEL,
|
|
426
|
+
name: AGENT_MODEL,
|
|
427
|
+
api: "openai-completions",
|
|
428
|
+
provider: "link", // non-builtin -> uses model.baseUrl (our localhost bridge) directly
|
|
429
|
+
baseUrl: this.bridge.url,
|
|
430
|
+
reasoning: false,
|
|
431
|
+
input: ["text"],
|
|
432
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
433
|
+
contextWindow,
|
|
434
|
+
maxTokens: maxOutputTokens,
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const tools = this.buildTools(frame.tools ?? []);
|
|
438
|
+
this.agent = new AgentCtor({
|
|
439
|
+
initialState: { systemPrompt: frame.system ?? "", model, tools },
|
|
440
|
+
getApiKey: () => "x",
|
|
441
|
+
// Real filesystem tools may race if run in parallel; keep them ordered. Steers drain
|
|
442
|
+
// as a batch ("all") so a burst of user messages doesn't each cost an extra turn.
|
|
443
|
+
toolExecution: "sequential",
|
|
444
|
+
steeringMode: "all",
|
|
445
|
+
});
|
|
446
|
+
this.agent.subscribe((event: any) => {
|
|
447
|
+
if (event.type === "turn_end") {
|
|
448
|
+
this.turns += 1;
|
|
449
|
+
if (this.turns >= this.turnCap && this.running) {
|
|
450
|
+
this.hitTurnCap = true;
|
|
451
|
+
this.agent.abort();
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
this.sendState("idle");
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private buildTools(specs: any[]): any[] {
|
|
459
|
+
const envTools = specs
|
|
460
|
+
.filter((t: any) => t.name !== "submit")
|
|
461
|
+
.map((t: any) => ({
|
|
462
|
+
name: t.name,
|
|
463
|
+
label: t.name,
|
|
464
|
+
description: t.description,
|
|
465
|
+
parameters: t.parameters,
|
|
466
|
+
execute: async (_id: string, params: any, signal?: AbortSignal) => {
|
|
467
|
+
if (signal?.aborted) {
|
|
468
|
+
return { content: [{ type: "text", text: "interrupted" }], details: {}, terminate: false };
|
|
469
|
+
}
|
|
470
|
+
const r = await this.conn.request("tool_request", { name: t.name, arguments: params });
|
|
471
|
+
// A host-side failure (budget exhausted, unknown tool, executor error) must reach
|
|
472
|
+
// the agent AS a failure: throw so pi records an error tool result, rather than
|
|
473
|
+
// letting the model reason from a failed action as if it succeeded.
|
|
474
|
+
if (r.is_error) {
|
|
475
|
+
throw new Error(String(r.content ?? "tool failed"));
|
|
476
|
+
}
|
|
477
|
+
return { content: [{ type: "text", text: String(r.content ?? "") }], details: r, terminate: false };
|
|
478
|
+
},
|
|
479
|
+
}));
|
|
480
|
+
const submit = {
|
|
481
|
+
name: "submit",
|
|
482
|
+
label: "submit",
|
|
483
|
+
description: "Finish the task and submit your answer/result summary. This ends the run.",
|
|
484
|
+
parameters: { type: "object", properties: { answer: { type: "string" } }, required: ["answer"] },
|
|
485
|
+
execute: async (_id: string, params: { answer?: string }, signal?: AbortSignal) => {
|
|
486
|
+
// Honor an interrupt racing a submit: don't emit a final submit for an aborted turn.
|
|
487
|
+
if (signal?.aborted) {
|
|
488
|
+
return { content: [{ type: "text", text: "interrupted" }], details: {}, terminate: false };
|
|
489
|
+
}
|
|
490
|
+
// The host emits the submit event; the run then ends (but the SESSION stays alive
|
|
491
|
+
// awaiting the next user message).
|
|
492
|
+
await this.conn.request("tool_request", { name: "submit", arguments: { answer: params.answer ?? "" } });
|
|
493
|
+
// Re-check after the round-trip: if the interrupt landed while the request was in
|
|
494
|
+
// flight, end via the abort path rather than terminating the run as a clean submit.
|
|
495
|
+
if (signal?.aborted) {
|
|
496
|
+
return { content: [{ type: "text", text: "interrupted" }], details: {}, terminate: false };
|
|
497
|
+
}
|
|
498
|
+
return { content: [{ type: "text", text: "submitted" }], details: {}, terminate: true };
|
|
499
|
+
},
|
|
500
|
+
};
|
|
501
|
+
return [...envTools, submit];
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
handleUserMessage(frame: Frame): void {
|
|
505
|
+
const text = String(frame.text ?? "");
|
|
506
|
+
if (!this.agent) return;
|
|
507
|
+
if (this.running) {
|
|
508
|
+
this.agent.steer(userMessage(text));
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
void this.runPrompt(text);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
private async runPrompt(text: string): Promise<void> {
|
|
515
|
+
this.running = true;
|
|
516
|
+
this.turns = 0;
|
|
517
|
+
this.interrupted = false;
|
|
518
|
+
this.hitTurnCap = false;
|
|
519
|
+
if (this.conversationScope === "turn") {
|
|
520
|
+
// AgentProject stores durable memory in its filesystem and gives every outer task a
|
|
521
|
+
// self-contained request. Retaining all prior task transcripts duplicates that state,
|
|
522
|
+
// grows input cost without bound, and makes pi clamp maxTokens to 1 near its context cap.
|
|
523
|
+
// Reset only here, while idle: tool calls within this logical turn still share context.
|
|
524
|
+
this.agent.state.messages = [];
|
|
525
|
+
}
|
|
526
|
+
this.sendState("running");
|
|
527
|
+
try {
|
|
528
|
+
await this.agent.prompt(text);
|
|
529
|
+
} catch (e) {
|
|
530
|
+
this.conn.send({ type: "episode_error", note: String(e) });
|
|
531
|
+
} finally {
|
|
532
|
+
this.running = false;
|
|
533
|
+
repairOrphanedToolCalls(this.agent);
|
|
534
|
+
const reason = this.hitTurnCap ? "turn_limit" : this.interrupted ? "aborted" : "completed";
|
|
535
|
+
this.sendState("idle", reason);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
handleAbort(_frame: Frame): void {
|
|
540
|
+
if (this.agent && this.running) {
|
|
541
|
+
this.interrupted = true;
|
|
542
|
+
// Interrupt cancels the WHOLE pending turn: clear any messages the user queued (via
|
|
543
|
+
// steer) before pressing Stop, so a follow-up typed just before the interrupt is not
|
|
544
|
+
// silently drained into the next prompt. New messages after this start a fresh turn.
|
|
545
|
+
this.agent.clearAllQueues?.();
|
|
546
|
+
this.agent.abort();
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
handlePing(frame: Frame): void {
|
|
551
|
+
this.conn.send({ type: "pong", nonce: frame.nonce });
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private sendState(status: "idle" | "running", reason?: string): void {
|
|
555
|
+
const frame: Frame = { type: "state", status, turns: this.turns };
|
|
556
|
+
if (reason) frame.reason = reason;
|
|
557
|
+
this.conn.send(frame);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function main(): void {
|
|
562
|
+
const conn = new StdioConn();
|
|
563
|
+
// AgentProject deliberately keeps this ordinary live session between proposal turns. Keep the
|
|
564
|
+
// output stream attached while idle too; host-side E2B timeout/idle-suspend policy is separate.
|
|
565
|
+
conn.startTransportKeepalive();
|
|
566
|
+
const session = new Session(conn);
|
|
567
|
+
conn.on("shutdown", () => process.exit(0));
|
|
568
|
+
conn.on("session_start", (start) => {
|
|
569
|
+
session.start(start).catch((e) => {
|
|
570
|
+
conn.send({ type: "episode_error", note: String(e) });
|
|
571
|
+
process.stderr.write(`[runner-live] start fatal ${e}\n`);
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
conn.on("user_message", (f) => session.handleUserMessage(f));
|
|
575
|
+
conn.on("abort", (f) => session.handleAbort(f));
|
|
576
|
+
conn.on("ping", (f) => session.handlePing(f));
|
|
577
|
+
conn.send({
|
|
578
|
+
type: "hello",
|
|
579
|
+
node_version: process.version,
|
|
580
|
+
pi_version: "0.80.3",
|
|
581
|
+
mode: "session",
|
|
582
|
+
transport: "stdio",
|
|
583
|
+
});
|
|
584
|
+
process.stderr.write("[runner-live] ready\n");
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
main();
|