@action-llama/action-llama 0.27.5 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/credential-setup.d.ts +0 -1
- package/dist/agents/credential-setup.d.ts.map +1 -1
- package/dist/agents/credential-setup.js +2 -23
- package/dist/agents/credential-setup.js.map +1 -1
- package/dist/agents/prompt.d.ts.map +1 -1
- package/dist/agents/prompt.js +12 -50
- package/dist/agents/prompt.js.map +1 -1
- package/dist/agents/scheduler-tools.d.ts +47 -0
- package/dist/agents/scheduler-tools.d.ts.map +1 -0
- package/dist/agents/scheduler-tools.js +276 -0
- package/dist/agents/scheduler-tools.js.map +1 -0
- package/dist/agents/status-reporter.d.ts +1 -1
- package/dist/agents/status-reporter.js +1 -1
- package/dist/agents/transport-runner.d.ts +123 -0
- package/dist/agents/transport-runner.d.ts.map +1 -0
- package/dist/agents/transport-runner.js +791 -0
- package/dist/agents/transport-runner.js.map +1 -0
- package/dist/build-info.json +1 -1
- package/dist/cli/commands/add.d.ts +1 -0
- package/dist/cli/commands/add.d.ts.map +1 -1
- package/dist/cli/commands/add.js +24 -9
- package/dist/cli/commands/add.js.map +1 -1
- package/dist/cli/commands/agent.d.ts +0 -3
- package/dist/cli/commands/agent.d.ts.map +1 -1
- package/dist/cli/commands/agent.js +3 -67
- package/dist/cli/commands/agent.js.map +1 -1
- package/dist/cli/commands/logs.d.ts.map +1 -1
- package/dist/cli/commands/logs.js +51 -339
- package/dist/cli/commands/logs.js.map +1 -1
- package/dist/cli/main.js +1 -30
- package/dist/cli/main.js.map +1 -1
- package/dist/control/routes/dashboard-api.d.ts.map +1 -1
- package/dist/control/routes/dashboard-api.js +3 -2
- package/dist/control/routes/dashboard-api.js.map +1 -1
- package/dist/control/routes/log-helpers.d.ts +4 -4
- package/dist/control/routes/log-helpers.d.ts.map +1 -1
- package/dist/control/routes/log-helpers.js +12 -7
- package/dist/control/routes/log-helpers.js.map +1 -1
- package/dist/control/routes/logs.d.ts.map +1 -1
- package/dist/control/routes/logs.js +10 -10
- package/dist/control/routes/logs.js.map +1 -1
- package/dist/control/session-store.js +1 -1
- package/dist/control/session-store.js.map +1 -1
- package/dist/docker/image.d.ts +26 -12
- package/dist/docker/image.d.ts.map +1 -1
- package/dist/docker/image.js +80 -88
- package/dist/docker/image.js.map +1 -1
- package/dist/docker/local-runtime.js +1 -1
- package/dist/docker/local-runtime.js.map +1 -1
- package/dist/docker/providers/index.d.ts +0 -4
- package/dist/docker/providers/index.d.ts.map +1 -1
- package/dist/docker/providers/index.js +0 -38
- package/dist/docker/providers/index.js.map +1 -1
- package/dist/docker/ssh-docker-runtime.js +1 -1
- package/dist/docker/ssh-docker-runtime.js.map +1 -1
- package/dist/execution/execution.d.ts +4 -2
- package/dist/execution/execution.d.ts.map +1 -1
- package/dist/execution/execution.js +14 -9
- package/dist/execution/execution.js.map +1 -1
- package/dist/execution/index.d.ts +1 -11
- package/dist/execution/index.d.ts.map +1 -1
- package/dist/execution/index.js +1 -8
- package/dist/execution/index.js.map +1 -1
- package/dist/execution/lifecycle/agent-lifecycle.d.ts +7 -0
- package/dist/execution/lifecycle/agent-lifecycle.d.ts.map +1 -1
- package/dist/execution/lifecycle/agent-lifecycle.js +65 -5
- package/dist/execution/lifecycle/agent-lifecycle.js.map +1 -1
- package/dist/execution/lifecycle/index.d.ts +2 -2
- package/dist/execution/lifecycle/index.d.ts.map +1 -1
- package/dist/execution/lifecycle/index.js +4 -2
- package/dist/execution/lifecycle/index.js.map +1 -1
- package/dist/execution/lifecycle/instance-lifecycle.d.ts +24 -0
- package/dist/execution/lifecycle/instance-lifecycle.d.ts.map +1 -1
- package/dist/execution/lifecycle/instance-lifecycle.js +41 -3
- package/dist/execution/lifecycle/instance-lifecycle.js.map +1 -1
- package/dist/execution/runner-setup.d.ts +9 -11
- package/dist/execution/runner-setup.d.ts.map +1 -1
- package/dist/execution/runner-setup.js +19 -14
- package/dist/execution/runner-setup.js.map +1 -1
- package/dist/execution/runtime-factory.d.ts +1 -15
- package/dist/execution/runtime-factory.d.ts.map +1 -1
- package/dist/execution/runtime-factory.js +1 -18
- package/dist/execution/runtime-factory.js.map +1 -1
- package/dist/execution/waiting-registry.d.ts +84 -0
- package/dist/execution/waiting-registry.d.ts.map +1 -0
- package/dist/execution/waiting-registry.js +185 -0
- package/dist/execution/waiting-registry.js.map +1 -0
- package/dist/frontend/assets/index-Bij4b7-g.js +16 -0
- package/dist/frontend/assets/index-Bij4b7-g.js.map +1 -0
- package/dist/frontend/assets/index-D11bfFWQ.css +2 -0
- package/dist/frontend/index.html +2 -2
- package/dist/gateway/index.d.ts +1 -1
- package/dist/gateway/index.d.ts.map +1 -1
- package/dist/gateway/index.js +6 -47
- package/dist/gateway/index.js.map +1 -1
- package/dist/gateway/routes/system.d.ts +1 -4
- package/dist/gateway/routes/system.d.ts.map +1 -1
- package/dist/gateway/routes/system.js +3 -8
- package/dist/gateway/routes/system.js.map +1 -1
- package/dist/gateway/stores.d.ts +0 -4
- package/dist/gateway/stores.d.ts.map +1 -1
- package/dist/gateway/stores.js +2 -10
- package/dist/gateway/stores.js.map +1 -1
- package/dist/gateway/types.d.ts +0 -13
- package/dist/gateway/types.d.ts.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +11 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/scheduler/gateway-setup.d.ts +0 -2
- package/dist/scheduler/gateway-setup.d.ts.map +1 -1
- package/dist/scheduler/gateway-setup.js +2 -11
- package/dist/scheduler/gateway-setup.js.map +1 -1
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +113 -55
- package/dist/scheduler/index.js.map +1 -1
- package/dist/scheduler/types.d.ts +1 -1
- package/dist/scheduler/types.d.ts.map +1 -1
- package/dist/scheduler/validation.js +1 -1
- package/dist/scheduler/validation.js.map +1 -1
- package/dist/scheduler/watcher.d.ts +2 -8
- package/dist/scheduler/watcher.d.ts.map +1 -1
- package/dist/scheduler/watcher.js +7 -104
- package/dist/scheduler/watcher.js.map +1 -1
- package/dist/shared/config/load-agent.js +2 -2
- package/dist/shared/config/load-agent.js.map +1 -1
- package/dist/shared/config/load-project.js +2 -2
- package/dist/shared/config/load-project.js.map +1 -1
- package/dist/shared/config/types.d.ts +12 -2
- package/dist/shared/config/types.d.ts.map +1 -1
- package/dist/shared/constants.d.ts +3 -1
- package/dist/shared/constants.d.ts.map +1 -1
- package/dist/shared/constants.js +4 -2
- package/dist/shared/constants.js.map +1 -1
- package/dist/shared/credential-refs.js +1 -1
- package/dist/shared/credential-refs.js.map +1 -1
- package/dist/shared/paths.d.ts.map +1 -1
- package/dist/shared/paths.js +2 -2
- package/dist/shared/paths.js.map +1 -1
- package/dist/shared/validation.js +1 -1
- package/dist/shared/validation.js.map +1 -1
- package/dist/transport/docker-exec.d.ts +41 -0
- package/dist/transport/docker-exec.d.ts.map +1 -0
- package/dist/transport/docker-exec.js +243 -0
- package/dist/transport/docker-exec.js.map +1 -0
- package/dist/transport/host-user.d.ts +37 -0
- package/dist/transport/host-user.d.ts.map +1 -0
- package/dist/transport/host-user.js +232 -0
- package/dist/transport/host-user.js.map +1 -0
- package/dist/transport/index.d.ts +8 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +7 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/memory.d.ts +36 -0
- package/dist/transport/memory.d.ts.map +1 -0
- package/dist/transport/memory.js +113 -0
- package/dist/transport/memory.js.map +1 -0
- package/dist/transport/operations.d.ts +68 -0
- package/dist/transport/operations.d.ts.map +1 -0
- package/dist/transport/operations.js +164 -0
- package/dist/transport/operations.js.map +1 -0
- package/dist/transport/ssh.d.ts +43 -0
- package/dist/transport/ssh.d.ts.map +1 -0
- package/dist/transport/ssh.js +225 -0
- package/dist/transport/ssh.js.map +1 -0
- package/dist/transport/transport.d.ts +61 -0
- package/dist/transport/transport.d.ts.map +1 -0
- package/dist/transport/transport.js +29 -0
- package/dist/transport/transport.js.map +1 -0
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +5 -4
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/status-tracker.d.ts +11 -2
- package/dist/tui/status-tracker.d.ts.map +1 -1
- package/dist/tui/status-tracker.js +44 -0
- package/dist/tui/status-tracker.js.map +1 -1
- package/package.json +7 -2
- package/dist/agents/bash-prefix.d.ts +0 -9
- package/dist/agents/bash-prefix.d.ts.map +0 -1
- package/dist/agents/bash-prefix.js +0 -25
- package/dist/agents/bash-prefix.js.map +0 -1
- package/dist/agents/container-entry.d.ts +0 -31
- package/dist/agents/container-entry.d.ts.map +0 -1
- package/dist/agents/container-entry.js +0 -302
- package/dist/agents/container-entry.js.map +0 -1
- package/dist/agents/container-runner.d.ts +0 -59
- package/dist/agents/container-runner.d.ts.map +0 -1
- package/dist/agents/container-runner.js +0 -472
- package/dist/agents/container-runner.js.map +0 -1
- package/dist/agents/harness/claude-cli-harness.d.ts +0 -15
- package/dist/agents/harness/claude-cli-harness.d.ts.map +0 -1
- package/dist/agents/harness/claude-cli-harness.js +0 -260
- package/dist/agents/harness/claude-cli-harness.js.map +0 -1
- package/dist/agents/harness/consumer.d.ts +0 -31
- package/dist/agents/harness/consumer.d.ts.map +0 -1
- package/dist/agents/harness/consumer.js +0 -165
- package/dist/agents/harness/consumer.js.map +0 -1
- package/dist/agents/harness/factory.d.ts +0 -9
- package/dist/agents/harness/factory.d.ts.map +0 -1
- package/dist/agents/harness/factory.js +0 -25
- package/dist/agents/harness/factory.js.map +0 -1
- package/dist/agents/harness/index.d.ts +0 -9
- package/dist/agents/harness/index.d.ts.map +0 -1
- package/dist/agents/harness/index.js +0 -5
- package/dist/agents/harness/index.js.map +0 -1
- package/dist/agents/harness/pi-harness.d.ts +0 -18
- package/dist/agents/harness/pi-harness.d.ts.map +0 -1
- package/dist/agents/harness/pi-harness.js +0 -278
- package/dist/agents/harness/pi-harness.js.map +0 -1
- package/dist/agents/harness/types.d.ts +0 -57
- package/dist/agents/harness/types.d.ts.map +0 -1
- package/dist/agents/harness/types.js +0 -2
- package/dist/agents/harness/types.js.map +0 -1
- package/dist/agents/session-loop.d.ts +0 -36
- package/dist/agents/session-loop.d.ts.map +0 -1
- package/dist/agents/session-loop.js +0 -216
- package/dist/agents/session-loop.js.map +0 -1
- package/dist/agents/signals.d.ts +0 -34
- package/dist/agents/signals.d.ts.map +0 -1
- package/dist/agents/signals.js +0 -122
- package/dist/agents/signals.js.map +0 -1
- package/dist/cli/commands/claude.d.ts +0 -4
- package/dist/cli/commands/claude.d.ts.map +0 -1
- package/dist/cli/commands/claude.js +0 -6
- package/dist/cli/commands/claude.js.map +0 -1
- package/dist/cli/commands/run-agent.d.ts +0 -14
- package/dist/cli/commands/run-agent.d.ts.map +0 -1
- package/dist/cli/commands/run-agent.js +0 -270
- package/dist/cli/commands/run-agent.js.map +0 -1
- package/dist/docker/cloud-run-runtime.d.ts +0 -48
- package/dist/docker/cloud-run-runtime.d.ts.map +0 -1
- package/dist/docker/cloud-run-runtime.js +0 -490
- package/dist/docker/cloud-run-runtime.js.map +0 -1
- package/dist/execution/call-dispatcher.d.ts +0 -11
- package/dist/execution/call-dispatcher.d.ts.map +0 -1
- package/dist/execution/call-dispatcher.js +0 -75
- package/dist/execution/call-dispatcher.js.map +0 -1
- package/dist/execution/container-registry.d.ts +0 -42
- package/dist/execution/container-registry.d.ts.map +0 -1
- package/dist/execution/container-registry.js +0 -76
- package/dist/execution/container-registry.js.map +0 -1
- package/dist/execution/image-builder.d.ts +0 -48
- package/dist/execution/image-builder.d.ts.map +0 -1
- package/dist/execution/image-builder.js +0 -155
- package/dist/execution/image-builder.js.map +0 -1
- package/dist/execution/routes/calls.d.ts +0 -18
- package/dist/execution/routes/calls.d.ts.map +0 -1
- package/dist/execution/routes/calls.js +0 -74
- package/dist/execution/routes/calls.js.map +0 -1
- package/dist/execution/routes/locks.d.ts +0 -10
- package/dist/execution/routes/locks.d.ts.map +0 -1
- package/dist/execution/routes/locks.js +0 -166
- package/dist/execution/routes/locks.js.map +0 -1
- package/dist/execution/routes/shutdown.d.ts +0 -5
- package/dist/execution/routes/shutdown.d.ts.map +0 -1
- package/dist/execution/routes/shutdown.js +0 -24
- package/dist/execution/routes/shutdown.js.map +0 -1
- package/dist/execution/routes/signals.d.ts +0 -12
- package/dist/execution/routes/signals.d.ts.map +0 -1
- package/dist/execution/routes/signals.js +0 -123
- package/dist/execution/routes/signals.js.map +0 -1
- package/dist/execution/types.d.ts +0 -23
- package/dist/execution/types.d.ts.map +0 -1
- package/dist/execution/types.js +0 -2
- package/dist/execution/types.js.map +0 -1
- package/dist/frontend/assets/index-DnSu-8Kw.js +0 -16
- package/dist/frontend/assets/index-DnSu-8Kw.js.map +0 -1
- package/dist/frontend/assets/index-Z0IOXEax.css +0 -2
- package/dist/gateway/routes/execution.d.ts +0 -24
- package/dist/gateway/routes/execution.d.ts.map +0 -1
- package/dist/gateway/routes/execution.js +0 -13
- package/dist/gateway/routes/execution.js.map +0 -1
- package/dist/scheduler/orphan-recovery.d.ts +0 -25
- package/dist/scheduler/orphan-recovery.d.ts.map +0 -1
- package/dist/scheduler/orphan-recovery.js +0 -144
- package/dist/scheduler/orphan-recovery.js.map +0 -1
- package/docker/bin/_http-exit +0 -35
- package/docker/bin/al-exit +0 -4
- package/docker/bin/al-export +0 -50
- package/docker/bin/al-rerun +0 -8
- package/docker/bin/al-return +0 -14
- package/docker/bin/al-shutdown +0 -5
- package/docker/bin/al-status +0 -9
- package/docker/bin/al-subagent +0 -13
- package/docker/bin/al-subagent-check +0 -10
- package/docker/bin/al-subagent-wait +0 -37
- package/docker/bin/rlock +0 -12
- package/docker/bin/rlock-heartbeat +0 -12
- package/docker/bin/runlock +0 -12
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TransportAgentRunner — runs Pi sessions in the scheduler process,
|
|
3
|
+
* driving a remote runtime via a Transport.
|
|
4
|
+
*
|
|
5
|
+
* Replaces ContainerAgentRunner for the centralized architecture where:
|
|
6
|
+
* - The scheduler is the "brain" (LLM session, tool orchestration)
|
|
7
|
+
* - The runtime is the "body" (filesystem, shell, credentials)
|
|
8
|
+
* - The transport connects them (DockerExecTransport, SshTransport, etc.)
|
|
9
|
+
*
|
|
10
|
+
* Key differences from ContainerAgentRunner:
|
|
11
|
+
* - Pi session runs in-process (not inside the container)
|
|
12
|
+
* - Events are captured directly (no JSON log parsing)
|
|
13
|
+
* - Credentials staged via transport (no volume mounts needed)
|
|
14
|
+
* - No gateway registration (direct access to scheduler services)
|
|
15
|
+
* - No signal files (scheduler tools handle reruns, returns, etc.)
|
|
16
|
+
*/
|
|
17
|
+
import { randomBytes } from "crypto";
|
|
18
|
+
import { execFileSync } from "child_process";
|
|
19
|
+
import { getModel } from "@mariozechner/pi-ai";
|
|
20
|
+
import { AuthStorage, createAgentSession, SessionManager, SettingsManager, DefaultResourceLoader, ModelRegistry, } from "@mariozechner/pi-coding-agent";
|
|
21
|
+
import { sessionStatsToUsage } from "../shared/usage.js";
|
|
22
|
+
import { DEFAULT_AGENT_TIMEOUT } from "../shared/constants.js";
|
|
23
|
+
import { selectAvailableModels, isRateLimitError } from "./model-fallback.js";
|
|
24
|
+
import { DockerExecTransport } from "../transport/docker-exec.js";
|
|
25
|
+
import { SshTransport } from "../transport/ssh.js";
|
|
26
|
+
import { HostUserTransport } from "../transport/host-user.js";
|
|
27
|
+
import { createTransportTools } from "../transport/operations.js";
|
|
28
|
+
import { parseCredentialRef, getDefaultBackend } from "../shared/credentials.js";
|
|
29
|
+
import { parseFrontmatter } from "../shared/frontmatter.js";
|
|
30
|
+
import { withSpan } from "../telemetry/index.js";
|
|
31
|
+
import { SpanKind } from "@opentelemetry/api";
|
|
32
|
+
import { createSchedulerTools } from "./scheduler-tools.js";
|
|
33
|
+
const MAX_MODEL_PASSES = 3;
|
|
34
|
+
const DEFAULT_BACKOFF_MS = 30_000;
|
|
35
|
+
const MAX_BACKOFF_MS = 300_000;
|
|
36
|
+
const CONTAINER_CWD = "/tmp";
|
|
37
|
+
export class TransportAgentRunner {
|
|
38
|
+
_running = false;
|
|
39
|
+
_suspended = false;
|
|
40
|
+
_aborting = false;
|
|
41
|
+
_transport = null;
|
|
42
|
+
_containerName = null;
|
|
43
|
+
_session = null;
|
|
44
|
+
_turnTextBuffer = "";
|
|
45
|
+
_turnThinkingBuffer = "";
|
|
46
|
+
instanceId;
|
|
47
|
+
/** Trigger depth for subagent call tracking. Set by the scheduler before run(). */
|
|
48
|
+
depth = 0;
|
|
49
|
+
globalConfig;
|
|
50
|
+
agentConfig;
|
|
51
|
+
baseLogger;
|
|
52
|
+
logger;
|
|
53
|
+
circuitBreaker;
|
|
54
|
+
statusTracker;
|
|
55
|
+
baseImage;
|
|
56
|
+
projectPath;
|
|
57
|
+
providerKeys;
|
|
58
|
+
schedulerToolsDeps;
|
|
59
|
+
waitingRegistry;
|
|
60
|
+
onWaitStateChange;
|
|
61
|
+
constructor(opts) {
|
|
62
|
+
this.globalConfig = opts.globalConfig;
|
|
63
|
+
this.agentConfig = opts.agentConfig;
|
|
64
|
+
this.baseLogger = opts.logger;
|
|
65
|
+
this.logger = opts.logger;
|
|
66
|
+
this.circuitBreaker = opts.circuitBreaker;
|
|
67
|
+
this.statusTracker = opts.statusTracker;
|
|
68
|
+
this.baseImage = opts.baseImage;
|
|
69
|
+
this.projectPath = opts.projectPath;
|
|
70
|
+
this.providerKeys = opts.providerKeys ?? new Map();
|
|
71
|
+
this.schedulerToolsDeps = opts.schedulerToolsDeps;
|
|
72
|
+
this.waitingRegistry = opts.waitingRegistry;
|
|
73
|
+
this.onWaitStateChange = opts.onWaitStateChange;
|
|
74
|
+
this.instanceId = opts.agentConfig.name;
|
|
75
|
+
}
|
|
76
|
+
get isRunning() {
|
|
77
|
+
return this._running;
|
|
78
|
+
}
|
|
79
|
+
setAgentConfig(config) {
|
|
80
|
+
this.agentConfig = config;
|
|
81
|
+
}
|
|
82
|
+
get isSuspended() {
|
|
83
|
+
return this._suspended;
|
|
84
|
+
}
|
|
85
|
+
abort() {
|
|
86
|
+
this._aborting = true;
|
|
87
|
+
this.logger.info("Transport agent runner abort requested");
|
|
88
|
+
// Dispose the Pi session to stop the LLM loop
|
|
89
|
+
if (this._session) {
|
|
90
|
+
try {
|
|
91
|
+
this._session.dispose();
|
|
92
|
+
}
|
|
93
|
+
catch { /* best effort */ }
|
|
94
|
+
}
|
|
95
|
+
// Unpause container first if suspended, then kill
|
|
96
|
+
if (this._containerName) {
|
|
97
|
+
if (this._suspended) {
|
|
98
|
+
try {
|
|
99
|
+
execFileSync("docker", ["unpause", this._containerName], {
|
|
100
|
+
timeout: 10_000,
|
|
101
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch { /* container may not be paused */ }
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
execFileSync("docker", ["kill", this._containerName], {
|
|
108
|
+
timeout: 10_000,
|
|
109
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch { /* container may already be dead */ }
|
|
113
|
+
}
|
|
114
|
+
// Close the transport
|
|
115
|
+
if (this._transport) {
|
|
116
|
+
this._transport.close().catch(() => { });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async run(prompt, triggerInfo, instanceId) {
|
|
120
|
+
if (this._running) {
|
|
121
|
+
this.logger.warn(`${this.agentConfig.name} is already running, skipping`);
|
|
122
|
+
return { result: "error", triggers: [] };
|
|
123
|
+
}
|
|
124
|
+
this._running = true;
|
|
125
|
+
this._aborting = false;
|
|
126
|
+
this.instanceId = instanceId ?? `${this.agentConfig.name}-${randomBytes(4).toString("hex")}`;
|
|
127
|
+
this.logger = this.baseLogger.child({ instance: this.instanceId });
|
|
128
|
+
try {
|
|
129
|
+
return await withSpan("transport_agent.run", async (span) => {
|
|
130
|
+
span.setAttributes({
|
|
131
|
+
"agent.name": this.agentConfig.name,
|
|
132
|
+
"agent.run_id": this.instanceId,
|
|
133
|
+
"agent.trigger_type": triggerInfo?.type || "manual",
|
|
134
|
+
"agent.trigger_source": triggerInfo?.source || "",
|
|
135
|
+
"agent.model_provider": this.agentConfig.models[0]?.provider,
|
|
136
|
+
"agent.model_name": this.agentConfig.models[0]?.model,
|
|
137
|
+
"execution.environment": "transport",
|
|
138
|
+
});
|
|
139
|
+
return this.runInternal(prompt, triggerInfo, span);
|
|
140
|
+
}, {}, SpanKind.INTERNAL);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
this._running = false;
|
|
144
|
+
this.logger.error({ err }, "transport run setup failed");
|
|
145
|
+
return { result: "error", triggers: [] };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async runInternal(prompt, triggerInfo, parentSpan) {
|
|
149
|
+
const runStartTime = Date.now();
|
|
150
|
+
let runResult = "error";
|
|
151
|
+
let runError;
|
|
152
|
+
let returnValue;
|
|
153
|
+
let tokenUsage;
|
|
154
|
+
// Surface run start in TUI
|
|
155
|
+
const runReason = triggerInfo
|
|
156
|
+
? (triggerInfo.source
|
|
157
|
+
? (triggerInfo.type === 'agent' ? `triggered by ${triggerInfo.source}` : `${triggerInfo.type} (${triggerInfo.source})`)
|
|
158
|
+
: triggerInfo.type)
|
|
159
|
+
: undefined;
|
|
160
|
+
this.statusTracker?.startRun(this.agentConfig.name, runReason);
|
|
161
|
+
this.statusTracker?.registerInstance({
|
|
162
|
+
id: this.instanceId,
|
|
163
|
+
agentName: this.agentConfig.name,
|
|
164
|
+
status: "running",
|
|
165
|
+
startedAt: new Date(),
|
|
166
|
+
trigger: triggerInfo?.source ? `${triggerInfo.type}:${triggerInfo.source}` : (triggerInfo?.type ?? "manual"),
|
|
167
|
+
});
|
|
168
|
+
this.logger.info(`Starting ${this.agentConfig.name} transport run`);
|
|
169
|
+
this.statusTracker?.addLogLine(this.agentConfig.name, `${this.instanceId} started (${runReason ?? "manual"})`);
|
|
170
|
+
// ── Timeout — kill the entire run if it exceeds the configured limit ──
|
|
171
|
+
const timeoutSeconds = this.agentConfig.timeout ?? this.globalConfig.local?.timeout ?? DEFAULT_AGENT_TIMEOUT;
|
|
172
|
+
const timeoutTimer = setTimeout(() => {
|
|
173
|
+
this.logger.error({ timeoutSeconds }, "agent timeout reached, aborting");
|
|
174
|
+
this.abort();
|
|
175
|
+
}, timeoutSeconds * 1000);
|
|
176
|
+
timeoutTimer.unref();
|
|
177
|
+
try {
|
|
178
|
+
// ── 1–2. Provision runtime & connect transport ──────────
|
|
179
|
+
const transport = await this.createTransport();
|
|
180
|
+
this._transport = transport;
|
|
181
|
+
// ── 3. Stage credentials ─────────────────────────────────
|
|
182
|
+
const providerKeys = await this.stageCredentials(transport);
|
|
183
|
+
// Merge with pre-configured provider keys
|
|
184
|
+
for (const [k, v] of this.providerKeys) {
|
|
185
|
+
if (!providerKeys.has(k))
|
|
186
|
+
providerKeys.set(k, v);
|
|
187
|
+
}
|
|
188
|
+
// ── 4. Run hooks.pre ─────────────────────────────────────
|
|
189
|
+
if (this.agentConfig.hooks?.pre && this.agentConfig.hooks.pre.length > 0) {
|
|
190
|
+
for (const hook of this.agentConfig.hooks.pre) {
|
|
191
|
+
this.logger.info({ hook }, "running pre hook via transport");
|
|
192
|
+
await transport.exec(hook);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// ── 5. Build SKILL.md and prompt ─────────────────────────
|
|
196
|
+
// Read SKILL.md from the project directory on the host
|
|
197
|
+
const { readFileSync, existsSync } = await import("fs");
|
|
198
|
+
const { join } = await import("path");
|
|
199
|
+
const skillPath = join(this.projectPath, "agents", this.agentConfig.name, "SKILL.md");
|
|
200
|
+
let skillBody = "";
|
|
201
|
+
if (existsSync(skillPath)) {
|
|
202
|
+
const { body } = parseFrontmatter(readFileSync(skillPath, "utf-8"));
|
|
203
|
+
skillBody = body;
|
|
204
|
+
}
|
|
205
|
+
// Process context injection (runs commands on the transport)
|
|
206
|
+
// TODO: In the future, context injection should execute on the transport
|
|
207
|
+
// For now, we skip it since the commands won't have access to the runtime
|
|
208
|
+
// ── 6. Create Pi session with transport tools ────────────
|
|
209
|
+
const result = await this.runPiSession(prompt, skillBody, transport, providerKeys);
|
|
210
|
+
runResult = result.result;
|
|
211
|
+
returnValue = result.returnValue;
|
|
212
|
+
tokenUsage = result.usage;
|
|
213
|
+
runError = result.error;
|
|
214
|
+
// ── 7. Run hooks.post ────────────────────────────────────
|
|
215
|
+
if (this.agentConfig.hooks?.post && this.agentConfig.hooks.post.length > 0) {
|
|
216
|
+
for (const hook of this.agentConfig.hooks.post) {
|
|
217
|
+
this.logger.info({ hook }, "running post hook via transport");
|
|
218
|
+
try {
|
|
219
|
+
await transport.exec(hook);
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
this.logger.error({ err }, "post hook failed");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
this.logger.error({ err }, `${this.agentConfig.name} transport run failed`);
|
|
229
|
+
runError = String(err?.message || err).slice(0, 200);
|
|
230
|
+
}
|
|
231
|
+
finally {
|
|
232
|
+
clearTimeout(timeoutTimer);
|
|
233
|
+
// ── 8. Cleanup ───────────────────────────────────────────
|
|
234
|
+
if (this._transport) {
|
|
235
|
+
try {
|
|
236
|
+
await this._transport.close();
|
|
237
|
+
}
|
|
238
|
+
catch { /* best effort */ }
|
|
239
|
+
this._transport = null;
|
|
240
|
+
}
|
|
241
|
+
if (this._containerName) {
|
|
242
|
+
try {
|
|
243
|
+
execFileSync("docker", ["rm", "-f", this._containerName], {
|
|
244
|
+
timeout: 10_000,
|
|
245
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
catch { /* best effort */ }
|
|
249
|
+
this._containerName = null;
|
|
250
|
+
}
|
|
251
|
+
const elapsed = Date.now() - runStartTime;
|
|
252
|
+
const instanceStatus = this._aborting ? "killed" : runError ? "error" : "completed";
|
|
253
|
+
this.statusTracker?.completeInstance(this.instanceId, instanceStatus);
|
|
254
|
+
this.statusTracker?.endRun(this.agentConfig.name, elapsed, runError, tokenUsage);
|
|
255
|
+
const elapsedStr = (elapsed / 1000).toFixed(1);
|
|
256
|
+
const turnInfo = tokenUsage?.turnCount != null ? `, ${tokenUsage.turnCount} turns` : "";
|
|
257
|
+
const costInfo = tokenUsage?.cost != null ? `, $${tokenUsage.cost.toFixed(4)}` : "";
|
|
258
|
+
const errInfo = runError ? ` — ${runError.slice(0, 100)}` : "";
|
|
259
|
+
this.statusTracker?.addLogLine(this.agentConfig.name, `${this.instanceId} ${runResult} (${elapsedStr}s${turnInfo}${costInfo})${errInfo}`);
|
|
260
|
+
this.logger.info({
|
|
261
|
+
result: runResult,
|
|
262
|
+
elapsed: `${elapsedStr}s`,
|
|
263
|
+
hasReturnValue: !!returnValue,
|
|
264
|
+
turnCount: tokenUsage?.turnCount,
|
|
265
|
+
inputTokens: tokenUsage?.inputTokens,
|
|
266
|
+
outputTokens: tokenUsage?.outputTokens,
|
|
267
|
+
totalTokens: tokenUsage?.totalTokens,
|
|
268
|
+
cost: tokenUsage?.cost,
|
|
269
|
+
error: runError,
|
|
270
|
+
}, "run outcome");
|
|
271
|
+
if (parentSpan) {
|
|
272
|
+
parentSpan.setAttributes({
|
|
273
|
+
"execution.result": runResult,
|
|
274
|
+
"execution.has_return_value": !!returnValue,
|
|
275
|
+
});
|
|
276
|
+
if (tokenUsage) {
|
|
277
|
+
parentSpan.setAttributes({
|
|
278
|
+
"llm.token.input": tokenUsage.inputTokens,
|
|
279
|
+
"llm.token.output": tokenUsage.outputTokens,
|
|
280
|
+
"llm.token.total": tokenUsage.totalTokens,
|
|
281
|
+
"llm.cost.total": tokenUsage.cost,
|
|
282
|
+
"llm.turns": tokenUsage.turnCount,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
if (runResult === "error") {
|
|
286
|
+
parentSpan.recordException(new Error(`Transport execution failed: ${runError || "Unknown error"}`));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
this._running = false;
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
result: runResult,
|
|
293
|
+
triggers: [],
|
|
294
|
+
returnValue,
|
|
295
|
+
usage: tokenUsage,
|
|
296
|
+
exitReason: runError,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Handle a wait_for_trigger call from the agent.
|
|
301
|
+
* Suspends the container, registers in the waiting registry, and returns
|
|
302
|
+
* a promise that resolves when a matching trigger arrives.
|
|
303
|
+
*/
|
|
304
|
+
async _handleWait(filter, timeoutMs, transport) {
|
|
305
|
+
if (!this.waitingRegistry) {
|
|
306
|
+
throw new Error("Waiting registry not available");
|
|
307
|
+
}
|
|
308
|
+
const runtimeType = this.agentConfig.runtime?.type ?? "container";
|
|
309
|
+
const deadline = Date.now() + timeoutMs;
|
|
310
|
+
// Disconnect transport and pause container
|
|
311
|
+
await transport.close();
|
|
312
|
+
this._suspended = true;
|
|
313
|
+
if (runtimeType === "container" && this._containerName) {
|
|
314
|
+
try {
|
|
315
|
+
execFileSync("docker", ["pause", this._containerName], {
|
|
316
|
+
timeout: 10_000,
|
|
317
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
318
|
+
});
|
|
319
|
+
this.logger.info({ container: this._containerName }, "container paused for wait");
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
this.logger.warn({ err: err.message }, "failed to pause container");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// Notify status tracker
|
|
326
|
+
this.statusTracker?.setInstanceWaiting(this.instanceId);
|
|
327
|
+
this.onWaitStateChange?.(this.instanceId, "waiting");
|
|
328
|
+
return new Promise((resolve, reject) => {
|
|
329
|
+
const entry = {
|
|
330
|
+
instanceId: this.instanceId,
|
|
331
|
+
agentName: this.agentConfig.name,
|
|
332
|
+
filter,
|
|
333
|
+
deadline,
|
|
334
|
+
registeredAt: Date.now(),
|
|
335
|
+
runId: this._containerName ?? this.instanceId,
|
|
336
|
+
runtimeType,
|
|
337
|
+
cwd: CONTAINER_CWD,
|
|
338
|
+
resolve: async (payload) => {
|
|
339
|
+
// Resume: unpause container, reconnect transport
|
|
340
|
+
try {
|
|
341
|
+
await this._resumeFromWait(transport);
|
|
342
|
+
resolve(payload);
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
reject(err);
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
reject: (err) => {
|
|
349
|
+
// Timeout or kill — still need to clean up
|
|
350
|
+
this._suspended = false;
|
|
351
|
+
this.onWaitStateChange?.(this.instanceId, "resumed");
|
|
352
|
+
reject(err);
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
this.waitingRegistry.register(entry);
|
|
356
|
+
this.logger.info({ filter, deadline: new Date(deadline).toISOString() }, "instance registered for wait");
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Resume from a suspended wait state: unpause container and reconnect transport.
|
|
361
|
+
*/
|
|
362
|
+
async _resumeFromWait(transport) {
|
|
363
|
+
const runtimeType = this.agentConfig.runtime?.type ?? "container";
|
|
364
|
+
if (runtimeType === "container" && this._containerName) {
|
|
365
|
+
try {
|
|
366
|
+
execFileSync("docker", ["unpause", this._containerName], {
|
|
367
|
+
timeout: 10_000,
|
|
368
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
369
|
+
});
|
|
370
|
+
this.logger.info({ container: this._containerName }, "container unpaused after wait");
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
this.logger.warn({ err: err.message }, "failed to unpause container");
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// Reconnect the transport
|
|
377
|
+
await transport.connect();
|
|
378
|
+
this._suspended = false;
|
|
379
|
+
this.statusTracker?.resumeInstance(this.instanceId);
|
|
380
|
+
this.onWaitStateChange?.(this.instanceId, "resumed");
|
|
381
|
+
this.logger.info("transport reconnected after wait resume");
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Create and connect the appropriate transport based on the agent's runtime config.
|
|
385
|
+
*/
|
|
386
|
+
async createTransport() {
|
|
387
|
+
const runtimeType = this.agentConfig.runtime?.type ?? "container";
|
|
388
|
+
switch (runtimeType) {
|
|
389
|
+
case "ssh": {
|
|
390
|
+
const rt = this.agentConfig.runtime;
|
|
391
|
+
if (!rt.host)
|
|
392
|
+
throw new Error("SSH runtime requires 'host' in [runtime] config");
|
|
393
|
+
const transport = new SshTransport({
|
|
394
|
+
host: rt.host,
|
|
395
|
+
port: rt.port,
|
|
396
|
+
user: rt.user,
|
|
397
|
+
keyPath: rt.key_path,
|
|
398
|
+
cwd: rt.cwd,
|
|
399
|
+
sshOptions: rt.ssh_options,
|
|
400
|
+
});
|
|
401
|
+
await transport.connect();
|
|
402
|
+
this.logger.debug({ host: rt.host, user: rt.user }, "SSH transport connected");
|
|
403
|
+
return transport;
|
|
404
|
+
}
|
|
405
|
+
case "host-user": {
|
|
406
|
+
const rt = this.agentConfig.runtime ?? {};
|
|
407
|
+
const user = rt.run_as ?? "al-agent";
|
|
408
|
+
const transport = new HostUserTransport({
|
|
409
|
+
user,
|
|
410
|
+
groups: rt.groups,
|
|
411
|
+
cwd: rt.cwd,
|
|
412
|
+
});
|
|
413
|
+
await transport.connect();
|
|
414
|
+
this.logger.debug({ user }, "Host-user transport connected");
|
|
415
|
+
return transport;
|
|
416
|
+
}
|
|
417
|
+
case "container":
|
|
418
|
+
default: {
|
|
419
|
+
const containerName = await this.provisionContainer();
|
|
420
|
+
this._containerName = containerName;
|
|
421
|
+
const transport = new DockerExecTransport({
|
|
422
|
+
container: containerName,
|
|
423
|
+
cwd: CONTAINER_CWD,
|
|
424
|
+
});
|
|
425
|
+
await transport.connect();
|
|
426
|
+
this.logger.debug({ container: containerName }, "Docker transport connected");
|
|
427
|
+
return transport;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Provision a Docker container that stays alive for the transport to connect to.
|
|
433
|
+
* Returns the container name.
|
|
434
|
+
*/
|
|
435
|
+
async provisionContainer() {
|
|
436
|
+
const runId = randomBytes(4).toString("hex");
|
|
437
|
+
const containerName = `al-${this.agentConfig.name}-${runId}`;
|
|
438
|
+
const memory = this.globalConfig.local?.memory || "4g";
|
|
439
|
+
// Build agent-specific image if a Dockerfile exists, otherwise use base
|
|
440
|
+
const { ensureAgentImage } = await import("../docker/image.js");
|
|
441
|
+
const image = await ensureAgentImage(this.agentConfig.name, this.projectPath, this.baseImage, (msg) => {
|
|
442
|
+
this.logger.debug(msg);
|
|
443
|
+
this.statusTracker?.addLogLine(this.agentConfig.name, msg);
|
|
444
|
+
});
|
|
445
|
+
// Ensure the image is available locally
|
|
446
|
+
this.ensureImageAvailable(image);
|
|
447
|
+
const args = [
|
|
448
|
+
"run", "-d",
|
|
449
|
+
"--name", containerName,
|
|
450
|
+
"--tmpfs", "/tmp:rw,exec,nosuid",
|
|
451
|
+
"--tmpfs", "/credentials:rw,nosuid,nodev,noexec",
|
|
452
|
+
"--cap-drop", "ALL",
|
|
453
|
+
"--security-opt", "no-new-privileges:true",
|
|
454
|
+
"--memory", memory,
|
|
455
|
+
];
|
|
456
|
+
if (this.globalConfig.local?.cpus) {
|
|
457
|
+
args.push("--cpus", String(this.globalConfig.local.cpus));
|
|
458
|
+
}
|
|
459
|
+
// Keep the container alive with tail -f /dev/null
|
|
460
|
+
args.push(image, "tail", "-f", "/dev/null");
|
|
461
|
+
execFileSync("docker", args, {
|
|
462
|
+
timeout: 30_000,
|
|
463
|
+
encoding: "utf-8",
|
|
464
|
+
});
|
|
465
|
+
this.logger.debug({ container: containerName }, "container provisioned");
|
|
466
|
+
return containerName;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Ensure a Docker image is available locally. If not, attempt to pull it.
|
|
470
|
+
*/
|
|
471
|
+
ensureImageAvailable(image) {
|
|
472
|
+
try {
|
|
473
|
+
execFileSync("docker", ["image", "inspect", image], {
|
|
474
|
+
timeout: 10_000,
|
|
475
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
// Image not found locally — try to pull
|
|
480
|
+
this.logger.debug({ image }, "image not found locally, pulling...");
|
|
481
|
+
try {
|
|
482
|
+
execFileSync("docker", ["pull", image], {
|
|
483
|
+
timeout: 120_000,
|
|
484
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
catch (pullErr) {
|
|
488
|
+
throw new Error(`Base image "${image}" not found and pull failed: ${pullErr.message}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Stage credentials onto the runtime via the transport.
|
|
494
|
+
* Returns a map of provider → API key for Pi session auth.
|
|
495
|
+
*/
|
|
496
|
+
async stageCredentials(transport) {
|
|
497
|
+
const providerKeys = new Map();
|
|
498
|
+
const credRefs = [...new Set(this.agentConfig.credentials ?? [])];
|
|
499
|
+
// Add provider keys for all configured models
|
|
500
|
+
for (const mc of this.agentConfig.models) {
|
|
501
|
+
if (mc.authType === "pi_auth")
|
|
502
|
+
continue;
|
|
503
|
+
const providerKey = `${mc.provider}_key`;
|
|
504
|
+
if (!credRefs.some((r) => r === providerKey || r.startsWith(`${providerKey}:`))) {
|
|
505
|
+
credRefs.push(providerKey);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (credRefs.length === 0)
|
|
509
|
+
return providerKeys;
|
|
510
|
+
// For host-user runtime, stage credentials to a temp dir (not /credentials which is root-only).
|
|
511
|
+
// For container runtimes, /credentials is inside the container filesystem.
|
|
512
|
+
const runtimeType = this.agentConfig.runtime?.type ?? "container";
|
|
513
|
+
const isHostUser = runtimeType === "host-user";
|
|
514
|
+
const credBase = isHostUser
|
|
515
|
+
? `/tmp/al-creds-${randomBytes(4).toString("hex")}`
|
|
516
|
+
: "/credentials";
|
|
517
|
+
const backend = getDefaultBackend();
|
|
518
|
+
const credFiles = new Map();
|
|
519
|
+
for (const credRef of credRefs) {
|
|
520
|
+
const { type, instance } = parseCredentialRef(credRef);
|
|
521
|
+
const fields = await backend.readAll(type, instance);
|
|
522
|
+
if (!fields)
|
|
523
|
+
continue;
|
|
524
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
525
|
+
// Stage credential file on the runtime
|
|
526
|
+
const remotePath = `${credBase}/${type}/${instance}/${field}`;
|
|
527
|
+
credFiles.set(remotePath, Buffer.from(value + "\n", "utf-8"));
|
|
528
|
+
// Extract provider API keys for Pi session auth
|
|
529
|
+
if (field === "api_key" || field === "token") {
|
|
530
|
+
if (type.endsWith("_key")) {
|
|
531
|
+
const provider = type.replace(/_key$/, "");
|
|
532
|
+
providerKeys.set(provider, value);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// Batch write all credential files
|
|
538
|
+
if (credFiles.size > 0) {
|
|
539
|
+
// Clean and recreate credential directory for a fresh state
|
|
540
|
+
await transport.exec(`rm -rf ${credBase} 2>/dev/null; mkdir -p ${credBase}`);
|
|
541
|
+
await transport.writeFiles(credFiles);
|
|
542
|
+
await transport.exec(`find ${credBase} -type f -exec chmod 400 {} +`);
|
|
543
|
+
this.logger.debug({ count: credFiles.size, credBase }, "credentials staged via transport");
|
|
544
|
+
}
|
|
545
|
+
return providerKeys;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Run the Pi session in-process with transport-backed tools.
|
|
549
|
+
*/
|
|
550
|
+
async runPiSession(prompt, skillBody, transport, providerKeys) {
|
|
551
|
+
const models = this.agentConfig.models;
|
|
552
|
+
// Create resource loader with the skill body
|
|
553
|
+
const agentsContent = skillBody || `# ${this.agentConfig.name} Agent\n\nCustom agent.\n`;
|
|
554
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
555
|
+
noExtensions: true,
|
|
556
|
+
agentsFilesOverride: () => ({
|
|
557
|
+
agentsFiles: [
|
|
558
|
+
{ path: "/tmp/SKILL.md", content: agentsContent },
|
|
559
|
+
],
|
|
560
|
+
}),
|
|
561
|
+
});
|
|
562
|
+
await resourceLoader.reload();
|
|
563
|
+
const settingsManager = SettingsManager.inMemory({
|
|
564
|
+
compaction: { enabled: true },
|
|
565
|
+
retry: { enabled: true, maxRetries: 2 },
|
|
566
|
+
});
|
|
567
|
+
// Create transport-backed tools
|
|
568
|
+
const tools = createTransportTools(transport, CONTAINER_CWD);
|
|
569
|
+
// Create scheduler tools (locks, calls, status, return, wait) if deps provided
|
|
570
|
+
let capturedReturnValue;
|
|
571
|
+
const customTools = this.schedulerToolsDeps
|
|
572
|
+
? createSchedulerTools({
|
|
573
|
+
...this.schedulerToolsDeps,
|
|
574
|
+
agentName: this.agentConfig.name,
|
|
575
|
+
instanceId: this.instanceId,
|
|
576
|
+
depth: this.depth,
|
|
577
|
+
onReturnValue: (value) => { capturedReturnValue = value; },
|
|
578
|
+
onWait: this.waitingRegistry ? (filter, timeoutMs) => this._handleWait(filter, timeoutMs, transport) : undefined,
|
|
579
|
+
defaultWaitTimeout: this.agentConfig.waitTimeout ?? this.globalConfig.defaultWaitTimeout,
|
|
580
|
+
})
|
|
581
|
+
: undefined;
|
|
582
|
+
let anyModelSucceeded = false;
|
|
583
|
+
for (let pass = 0; pass <= MAX_MODEL_PASSES; pass++) {
|
|
584
|
+
const availableModels = selectAvailableModels(models, this.circuitBreaker);
|
|
585
|
+
let modelSucceeded = false;
|
|
586
|
+
for (const modelConfig of availableModels) {
|
|
587
|
+
if (this._aborting) {
|
|
588
|
+
return { result: "error", error: "Aborted" };
|
|
589
|
+
}
|
|
590
|
+
const authStorage = AuthStorage.create();
|
|
591
|
+
const providerKey = providerKeys.get(modelConfig.provider);
|
|
592
|
+
if (providerKey) {
|
|
593
|
+
authStorage.setRuntimeApiKey(modelConfig.provider, providerKey);
|
|
594
|
+
}
|
|
595
|
+
// Resolve model — either from built-in registry or custom provider with baseUrl
|
|
596
|
+
let llmModel;
|
|
597
|
+
let customModelRegistry;
|
|
598
|
+
if (modelConfig.baseUrl) {
|
|
599
|
+
// Create a model registry with the custom provider registered
|
|
600
|
+
customModelRegistry = ModelRegistry.inMemory(authStorage);
|
|
601
|
+
const providerName = `custom_${modelConfig.provider}`;
|
|
602
|
+
// Register API key under custom provider name too
|
|
603
|
+
if (providerKey) {
|
|
604
|
+
authStorage.setRuntimeApiKey(providerName, providerKey);
|
|
605
|
+
}
|
|
606
|
+
customModelRegistry.registerProvider(providerName, {
|
|
607
|
+
baseUrl: modelConfig.baseUrl,
|
|
608
|
+
apiKey: providerKey || "dummy-key",
|
|
609
|
+
models: [{
|
|
610
|
+
id: modelConfig.model,
|
|
611
|
+
name: modelConfig.model,
|
|
612
|
+
api: "openai-completions",
|
|
613
|
+
reasoning: false,
|
|
614
|
+
input: ["text"],
|
|
615
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
616
|
+
contextWindow: 128000,
|
|
617
|
+
maxTokens: 16384,
|
|
618
|
+
}],
|
|
619
|
+
});
|
|
620
|
+
llmModel = customModelRegistry.find(providerName, modelConfig.model);
|
|
621
|
+
if (!llmModel) {
|
|
622
|
+
throw new Error(`Failed to register custom model ${modelConfig.provider}/${modelConfig.model} at ${modelConfig.baseUrl}`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
llmModel = getModel(modelConfig.provider, modelConfig.model);
|
|
627
|
+
}
|
|
628
|
+
this.logger.debug({
|
|
629
|
+
model: modelConfig.model,
|
|
630
|
+
thinking: modelConfig.thinkingLevel,
|
|
631
|
+
baseUrl: modelConfig.baseUrl,
|
|
632
|
+
}, "creating Pi session with transport tools");
|
|
633
|
+
const { session } = await createAgentSession({
|
|
634
|
+
cwd: CONTAINER_CWD,
|
|
635
|
+
model: llmModel,
|
|
636
|
+
modelRegistry: customModelRegistry,
|
|
637
|
+
thinkingLevel: modelConfig.thinkingLevel,
|
|
638
|
+
authStorage,
|
|
639
|
+
resourceLoader,
|
|
640
|
+
tools,
|
|
641
|
+
customTools,
|
|
642
|
+
sessionManager: SessionManager.inMemory(),
|
|
643
|
+
settingsManager,
|
|
644
|
+
});
|
|
645
|
+
this._session = session;
|
|
646
|
+
// Subscribe to events for logging and status tracking
|
|
647
|
+
this.subscribeToEvents(session);
|
|
648
|
+
try {
|
|
649
|
+
const state = session.state;
|
|
650
|
+
this.logger.debug({
|
|
651
|
+
promptLength: prompt.length,
|
|
652
|
+
model: state?.model ? { id: state.model.id, provider: state.model.provider, api: state.model.api, baseUrl: state.model.baseUrl } : "NO MODEL",
|
|
653
|
+
toolCount: state?.tools?.length ?? 0,
|
|
654
|
+
}, "about to call session.prompt()");
|
|
655
|
+
await session.prompt(prompt);
|
|
656
|
+
this.flushTurnBuffers();
|
|
657
|
+
const allMessages = session.state?.messages ?? [];
|
|
658
|
+
const lastMsg = allMessages[allMessages.length - 1];
|
|
659
|
+
if (lastMsg?.stopReason === "error") {
|
|
660
|
+
this.logger.error({
|
|
661
|
+
errorMessage: lastMsg?.errorMessage ?? session.state?.errorMessage,
|
|
662
|
+
}, "session.prompt() completed with error");
|
|
663
|
+
}
|
|
664
|
+
this.circuitBreaker.recordSuccess(modelConfig.provider, modelConfig.model);
|
|
665
|
+
// Get usage stats (logged later in "run outcome")
|
|
666
|
+
const sessionStats = session.getSessionStats();
|
|
667
|
+
const usage = sessionStatsToUsage(sessionStats);
|
|
668
|
+
session.dispose();
|
|
669
|
+
this._session = null;
|
|
670
|
+
modelSucceeded = true;
|
|
671
|
+
anyModelSucceeded = true;
|
|
672
|
+
return { result: "completed", usage, returnValue: capturedReturnValue };
|
|
673
|
+
}
|
|
674
|
+
catch (promptErr) {
|
|
675
|
+
const msg = String(promptErr?.message || promptErr || "");
|
|
676
|
+
if (isRateLimitError(msg)) {
|
|
677
|
+
this.circuitBreaker.recordFailure(modelConfig.provider, modelConfig.model);
|
|
678
|
+
this.logger.warn({
|
|
679
|
+
provider: modelConfig.provider,
|
|
680
|
+
model: modelConfig.model,
|
|
681
|
+
}, "rate limited, trying next model");
|
|
682
|
+
session.dispose();
|
|
683
|
+
this._session = null;
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
session.dispose();
|
|
687
|
+
this._session = null;
|
|
688
|
+
return { result: "error", error: msg };
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (modelSucceeded)
|
|
692
|
+
break;
|
|
693
|
+
if (pass < MAX_MODEL_PASSES) {
|
|
694
|
+
const delayMs = Math.min(DEFAULT_BACKOFF_MS * Math.pow(2, pass), MAX_BACKOFF_MS);
|
|
695
|
+
this.logger.warn({ pass: pass + 1, delayMs }, "all models exhausted, backing off");
|
|
696
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (!anyModelSucceeded) {
|
|
700
|
+
this.logger.error("all models exhausted across all retry passes");
|
|
701
|
+
return { result: "error", error: "All models exhausted — rate limited across all retries" };
|
|
702
|
+
}
|
|
703
|
+
return { result: "completed" };
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Subscribe to Pi session events and forward them to the logger and status tracker.
|
|
707
|
+
* Since the session runs in-process, we get direct access to events.
|
|
708
|
+
*/
|
|
709
|
+
subscribeToEvents(session) {
|
|
710
|
+
session.subscribe((event) => {
|
|
711
|
+
this.logger.debug({ eventType: event.type }, "session event");
|
|
712
|
+
if (event.type === "tool_execution_start") {
|
|
713
|
+
const toolName = String(event.toolName || "unknown");
|
|
714
|
+
const summary = toolName === "bash"
|
|
715
|
+
? String(event.args?.command || "").slice(0, 200)
|
|
716
|
+
: JSON.stringify(event.args ?? {}).slice(0, 200);
|
|
717
|
+
this.logger.info({ toolName, summary }, "[tool]");
|
|
718
|
+
}
|
|
719
|
+
if (event.type === "tool_execution_end") {
|
|
720
|
+
const toolName = String(event.toolName || "unknown");
|
|
721
|
+
const resultText = this.extractToolResultText(event.result);
|
|
722
|
+
if (event.isError) {
|
|
723
|
+
const errorSummary = resultText.slice(0, 200);
|
|
724
|
+
this.statusTracker?.setAgentError(this.agentConfig.name, errorSummary);
|
|
725
|
+
this.logger.warn({ toolName, error: errorSummary }, "[error]");
|
|
726
|
+
}
|
|
727
|
+
else if (resultText) {
|
|
728
|
+
this.logger.info({ toolName, summary: resultText.slice(0, 200) }, "[result]");
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
// Pi SDK emits thinking/text as message_update events with assistantMessageEvent
|
|
732
|
+
if (event.type === "message_update" && event.assistantMessageEvent) {
|
|
733
|
+
const ame = event.assistantMessageEvent;
|
|
734
|
+
if (ame.type === "thinking_delta") {
|
|
735
|
+
const delta = String(ame.delta ?? "");
|
|
736
|
+
if (delta)
|
|
737
|
+
this._turnThinkingBuffer += delta;
|
|
738
|
+
}
|
|
739
|
+
if (ame.type === "text_delta") {
|
|
740
|
+
const delta = String(ame.delta ?? ame.content ?? "");
|
|
741
|
+
if (delta)
|
|
742
|
+
this._turnTextBuffer += delta;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (event.type === "turn_end") {
|
|
746
|
+
this.flushTurnBuffers();
|
|
747
|
+
}
|
|
748
|
+
if (event.type === "error") {
|
|
749
|
+
const err = event.error;
|
|
750
|
+
const errorMsg = err?.errorMessage
|
|
751
|
+
|| (typeof err === "string" ? err : null)
|
|
752
|
+
|| JSON.stringify(event);
|
|
753
|
+
this.logger.error({ error: String(errorMsg).slice(0, 300) }, "[error]");
|
|
754
|
+
this.statusTracker?.setAgentError(this.agentConfig.name, String(errorMsg).slice(0, 200));
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Extract human-readable text from a tool result, unwrapping the Pi SDK content array format.
|
|
760
|
+
* Returns empty string for empty/trivial results.
|
|
761
|
+
*/
|
|
762
|
+
extractToolResultText(result) {
|
|
763
|
+
if (result == null)
|
|
764
|
+
return "";
|
|
765
|
+
if (typeof result === "string")
|
|
766
|
+
return result.trim();
|
|
767
|
+
// Pi SDK wraps results as { content: [{ type: "text", text: "..." }] }
|
|
768
|
+
const obj = result;
|
|
769
|
+
if (obj.content && Array.isArray(obj.content)) {
|
|
770
|
+
const texts = obj.content
|
|
771
|
+
.filter((c) => c.type === "text" && c.text)
|
|
772
|
+
.map((c) => String(c.text).trim())
|
|
773
|
+
.filter(Boolean);
|
|
774
|
+
return texts.join("\n");
|
|
775
|
+
}
|
|
776
|
+
const str = JSON.stringify(result);
|
|
777
|
+
return str === "{}" || str === "[]" ? "" : str;
|
|
778
|
+
}
|
|
779
|
+
/** Flush accumulated text/thinking buffers to the logger. */
|
|
780
|
+
flushTurnBuffers() {
|
|
781
|
+
if (this._turnThinkingBuffer) {
|
|
782
|
+
this.logger.info({ text: this._turnThinkingBuffer.slice(0, 2000) }, "[thinking]");
|
|
783
|
+
this._turnThinkingBuffer = "";
|
|
784
|
+
}
|
|
785
|
+
if (this._turnTextBuffer) {
|
|
786
|
+
this.logger.info({ text: this._turnTextBuffer.slice(0, 2000) }, "[text]");
|
|
787
|
+
this._turnTextBuffer = "";
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
//# sourceMappingURL=transport-runner.js.map
|