@plainconceptsplatform/loop-task 2.6.0 → 2.6.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 +1 -1
- package/README.md +31 -31
- package/dist/client/project-commands.js +1 -1
- package/dist/core/command/command-runner.js +69 -2
- package/dist/core/context/context-parser.js +86 -1
- package/dist/core/context/opencode-json-parser.js +129 -0
- package/dist/core/context/template.js +20 -2
- package/dist/core/context/types.js +1 -0
- package/dist/core/loop/chain-executor.js +4 -1
- package/dist/core/loop/run-executor.js +5 -1
- package/dist/daemon/http/openapi.js +2 -2
- package/dist/daemon/index.js +9 -0
- package/dist/daemon/managers/project-manager.js +1 -1
- package/dist/daemon/telemetry/agent-integrations/claude-code-integration.js +6 -0
- package/dist/daemon/telemetry/agent-integrations/opencode-integration.js +177 -6
- package/dist/daemon/telemetry/agent-serve-manager.js +78 -0
- package/dist/daemon/telemetry/noop-telemetry-adapter.js +1 -0
- package/dist/daemon/telemetry/open-telemetry-adapter.js +101 -22
- package/dist/entry.js +0 -0
- package/dist/shared/i18n/en.json +1 -1
- package/package.json +118 -118
|
@@ -1,11 +1,24 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
import { daemonLog } from "../../daemon-log.js";
|
|
1
3
|
const OPENCODE_BINARIES = ["opencode"];
|
|
4
|
+
const SERVE_PORT = 4096;
|
|
5
|
+
const SERVE_URL = `http://localhost:${SERVE_PORT}`;
|
|
6
|
+
const HEALTH_CHECK_TIMEOUT_MS = 120_000;
|
|
7
|
+
const HEALTH_CHECK_INTERVAL_MS = 1_000;
|
|
8
|
+
const SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
2
9
|
/**
|
|
3
10
|
* Detects `opencode run ...` invocations and enables OpenCode's
|
|
4
11
|
* native OpenTelemetry support, routing to loop-task's endpoint.
|
|
12
|
+
*
|
|
13
|
+
* Also manages a persistent `opencode serve` sidecar process so that
|
|
14
|
+
* `opencode run` tasks delegate to a warm serve instance via `--attach`,
|
|
15
|
+
* eliminating per-task cold-start time.
|
|
5
16
|
*/
|
|
6
17
|
export class OpenCodeTelemetryIntegration {
|
|
7
18
|
constructor() {
|
|
8
19
|
this.id = "opencode";
|
|
20
|
+
this.serveProcess = null;
|
|
21
|
+
this.serveInfo = null;
|
|
9
22
|
}
|
|
10
23
|
matches(command, args) {
|
|
11
24
|
const basename = command.split("/").pop()?.replace(/\.exe$/, "").toLowerCase() ?? "";
|
|
@@ -15,19 +28,53 @@ export class OpenCodeTelemetryIntegration {
|
|
|
15
28
|
const env = { ...invocation.env };
|
|
16
29
|
// Enable OpenCode's experimental OpenTelemetry
|
|
17
30
|
env.OPENCODE_EXPERIMENTAL_OPEN_TELEMETRY = "true";
|
|
18
|
-
// Adapt the OTLP endpoint/proprotocol from the parent context
|
|
19
|
-
// OpenCode currently reads the standard OTEL_* environment variables
|
|
20
|
-
// which are already set by the adapter's prepareChildProcess
|
|
21
|
-
// We just need to make sure the integration is activated.
|
|
22
31
|
return { command: invocation.command, args: [...invocation.args], env };
|
|
23
32
|
}
|
|
24
33
|
parseUsage(result) {
|
|
25
34
|
if (result.exitCode !== 0 || !result.stdout)
|
|
26
35
|
return undefined;
|
|
27
36
|
try {
|
|
28
|
-
//
|
|
29
|
-
//
|
|
37
|
+
// With --format json, opencode emits JSONL. The last step_finish event
|
|
38
|
+
// has token/cost info. Try parsing as JSONL first.
|
|
30
39
|
const lines = result.stdout.trim().split("\n");
|
|
40
|
+
const jsonlLines = [];
|
|
41
|
+
let allJson = true;
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
try {
|
|
44
|
+
jsonlLines.push(JSON.parse(line));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
allJson = false;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (allJson && jsonlLines.length > 0) {
|
|
52
|
+
// Find the last step_finish event
|
|
53
|
+
let accumulated = {};
|
|
54
|
+
for (const evt of jsonlLines) {
|
|
55
|
+
if (typeof evt === "object" && evt !== null && evt.type === "step_finish") {
|
|
56
|
+
const part = evt.part;
|
|
57
|
+
const tokens = part?.tokens;
|
|
58
|
+
if (tokens) {
|
|
59
|
+
accumulated.inputTokens = (accumulated.inputTokens ?? 0) + (typeof tokens.input === "number" ? tokens.input : 0);
|
|
60
|
+
accumulated.outputTokens = (accumulated.outputTokens ?? 0) + (typeof tokens.output === "number" ? tokens.output : 0);
|
|
61
|
+
const cache = tokens.cache;
|
|
62
|
+
if (cache) {
|
|
63
|
+
accumulated.cacheReadTokens = (accumulated.cacheReadTokens ?? 0) + (typeof cache.read === "number" ? cache.read : 0);
|
|
64
|
+
accumulated.cacheWriteTokens = (accumulated.cacheWriteTokens ?? 0) + (typeof cache.write === "number" ? cache.write : 0);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (typeof part?.cost === "number") {
|
|
68
|
+
accumulated.costUsd = (accumulated.costUsd ?? 0) + part.cost;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (Object.keys(accumulated).length > 0) {
|
|
73
|
+
accumulated.integration = "opencode";
|
|
74
|
+
return accumulated;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Fallback: try parsing the last line as JSON (legacy --format json without JSONL)
|
|
31
78
|
const lastLine = lines[lines.length - 1];
|
|
32
79
|
if (!lastLine)
|
|
33
80
|
return undefined;
|
|
@@ -63,10 +110,134 @@ export class OpenCodeTelemetryIntegration {
|
|
|
63
110
|
usage.sessionId = parsed.sessionId;
|
|
64
111
|
if (Object.keys(usage).length === 0)
|
|
65
112
|
return undefined;
|
|
113
|
+
usage.integration = "opencode";
|
|
66
114
|
return usage;
|
|
67
115
|
}
|
|
68
116
|
catch {
|
|
69
117
|
return undefined;
|
|
70
118
|
}
|
|
71
119
|
}
|
|
120
|
+
async ensureServe(cwd, env) {
|
|
121
|
+
// Already running and healthy
|
|
122
|
+
if (this.serveInfo && this.isServeAlive()) {
|
|
123
|
+
return this.serveInfo;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
daemonLog("opencode-serve: starting opencode serve sidecar");
|
|
127
|
+
this.serveProcess = execa("opencode", [
|
|
128
|
+
"serve",
|
|
129
|
+
"--port", String(SERVE_PORT),
|
|
130
|
+
"--hostname", "127.0.0.1",
|
|
131
|
+
], {
|
|
132
|
+
cwd,
|
|
133
|
+
env: { ...process.env, ...env },
|
|
134
|
+
stdout: "pipe",
|
|
135
|
+
stderr: "pipe",
|
|
136
|
+
buffer: false,
|
|
137
|
+
detached: false,
|
|
138
|
+
});
|
|
139
|
+
this.serveProcess.catch((err) => {
|
|
140
|
+
daemonLog(`opencode-serve: process error: ${String(err)}`);
|
|
141
|
+
this.serveProcess = null;
|
|
142
|
+
this.serveInfo = null;
|
|
143
|
+
});
|
|
144
|
+
this.serveProcess.on("exit", (code) => {
|
|
145
|
+
daemonLog(`opencode-serve: process exited with code ${code}`);
|
|
146
|
+
this.serveProcess = null;
|
|
147
|
+
this.serveInfo = null;
|
|
148
|
+
});
|
|
149
|
+
// Wait for health check
|
|
150
|
+
const healthy = await this.waitForHealth();
|
|
151
|
+
if (healthy) {
|
|
152
|
+
const pid = this.serveProcess?.pid;
|
|
153
|
+
if (pid) {
|
|
154
|
+
this.serveInfo = { port: SERVE_PORT, pid, url: SERVE_URL };
|
|
155
|
+
daemonLog(`opencode-serve: healthy, pid=${pid}, port=${SERVE_PORT}`);
|
|
156
|
+
return this.serveInfo;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
daemonLog("opencode-serve: health check timed out");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
daemonLog(`opencode-serve: failed to start: ${String(err)}`);
|
|
164
|
+
this.serveProcess = null;
|
|
165
|
+
this.serveInfo = null;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
isServeAlive() {
|
|
170
|
+
// Fast check: if we don't have a process reference, serve is not alive
|
|
171
|
+
if (!this.serveProcess || !this.serveInfo)
|
|
172
|
+
return false;
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
prepareRunArgs(args, cwd) {
|
|
176
|
+
const modified = [...args];
|
|
177
|
+
// Inject --attach after "run" (args[0] === "run")
|
|
178
|
+
const runIndex = modified.indexOf("run");
|
|
179
|
+
if (runIndex === -1)
|
|
180
|
+
return modified;
|
|
181
|
+
// Check if --attach is already present
|
|
182
|
+
const hasAttach = modified.includes("--attach");
|
|
183
|
+
if (!hasAttach) {
|
|
184
|
+
modified.splice(runIndex + 1, 0, "--attach", SERVE_URL);
|
|
185
|
+
}
|
|
186
|
+
// Inject --dir so serve knows which directory to work in
|
|
187
|
+
const hasDir = modified.includes("--dir");
|
|
188
|
+
if (!hasDir && cwd) {
|
|
189
|
+
// Find the position after --attach (or after "run" if --attach wasn't added by us)
|
|
190
|
+
const attachIdx = modified.indexOf("--attach");
|
|
191
|
+
const insertPos = attachIdx >= 0 ? attachIdx + 2 : runIndex + 1;
|
|
192
|
+
modified.splice(insertPos, 0, "--dir", cwd);
|
|
193
|
+
}
|
|
194
|
+
return modified;
|
|
195
|
+
}
|
|
196
|
+
async shutdownServe() {
|
|
197
|
+
if (!this.serveProcess)
|
|
198
|
+
return;
|
|
199
|
+
daemonLog("opencode-serve: shutting down serve process");
|
|
200
|
+
const proc = this.serveProcess;
|
|
201
|
+
this.serveProcess = null;
|
|
202
|
+
this.serveInfo = null;
|
|
203
|
+
try {
|
|
204
|
+
proc.kill("SIGTERM");
|
|
205
|
+
await Promise.race([
|
|
206
|
+
proc,
|
|
207
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("shutdown timeout")), SHUTDOWN_TIMEOUT_MS)),
|
|
208
|
+
]);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// Force kill if SIGTERM didn't work
|
|
212
|
+
try {
|
|
213
|
+
proc.kill("SIGKILL");
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// best effort
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async waitForHealth() {
|
|
221
|
+
const start = Date.now();
|
|
222
|
+
while (Date.now() - start < HEALTH_CHECK_TIMEOUT_MS) {
|
|
223
|
+
if (!this.serveProcess)
|
|
224
|
+
return false; // process exited
|
|
225
|
+
try {
|
|
226
|
+
const controller = new AbortController();
|
|
227
|
+
const timeout = setTimeout(() => controller.abort(), 3_000);
|
|
228
|
+
const res = await fetch(`${SERVE_URL}/global/health`, { signal: controller.signal });
|
|
229
|
+
clearTimeout(timeout);
|
|
230
|
+
if (res.ok) {
|
|
231
|
+
const data = await res.json();
|
|
232
|
+
if (data.healthy === true)
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// server not ready yet
|
|
238
|
+
}
|
|
239
|
+
await new Promise((r) => setTimeout(r, HEALTH_CHECK_INTERVAL_MS));
|
|
240
|
+
}
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
72
243
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { daemonLog } from "../daemon-log.js";
|
|
2
|
+
import { getAgentIntegrations } from "./agent-integrations/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Manages the lifecycle of persistent agent serve sidecars (e.g., opencode serve).
|
|
5
|
+
*
|
|
6
|
+
* On daemon boot, starts serve processes for integrations that support them.
|
|
7
|
+
* On daemon shutdown, gracefully stops all serve processes.
|
|
8
|
+
*
|
|
9
|
+
* The serve manager is the single owner of serve process lifecycle.
|
|
10
|
+
* The TelemetryManager and command-runner reference the integration's
|
|
11
|
+
* isServeAlive() to check state, but never start/stop serve processes directly.
|
|
12
|
+
*/
|
|
13
|
+
export class AgentServeManager {
|
|
14
|
+
constructor(settings, telemetryManager) {
|
|
15
|
+
this.settings = settings;
|
|
16
|
+
this.telemetryManager = telemetryManager;
|
|
17
|
+
this.integrations = [];
|
|
18
|
+
this.integrations = [...getAgentIntegrations()];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Start serve sidecars for all integrations that support them.
|
|
22
|
+
* Called on daemon boot.
|
|
23
|
+
*/
|
|
24
|
+
async ensureAllServe(cwd) {
|
|
25
|
+
const endpoint = this.settings.telemetryEndpoint;
|
|
26
|
+
if (!endpoint) {
|
|
27
|
+
daemonLog("agent-serve-manager: no telemetry endpoint, skipping serve startup");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// Build static OTEL env for serve processes
|
|
31
|
+
const serveEnv = {
|
|
32
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: endpoint,
|
|
33
|
+
OTEL_EXPORTER_OTLP_PROTOCOL: this.settings.telemetryProtocol,
|
|
34
|
+
OTEL_TRACES_EXPORTER: "otlp",
|
|
35
|
+
OTEL_METRICS_EXPORTER: "otlp",
|
|
36
|
+
OTEL_LOGS_EXPORTER: "otlp",
|
|
37
|
+
OTEL_RESOURCE_ATTRIBUTES: `service.name=${this.settings.telemetryServiceName}`,
|
|
38
|
+
};
|
|
39
|
+
if (this.settings.telemetryAutoInstrumentAgents) {
|
|
40
|
+
serveEnv.OPENCODE_EXPERIMENTAL_OPEN_TELEMETRY = "true";
|
|
41
|
+
serveEnv.CLAUDE_CODE_ENABLE_TELEMETRY = "1";
|
|
42
|
+
}
|
|
43
|
+
for (const integration of this.integrations) {
|
|
44
|
+
if (!integration.ensureServe)
|
|
45
|
+
continue;
|
|
46
|
+
try {
|
|
47
|
+
daemonLog(`agent-serve-manager: ensuring serve for integration "${integration.id}"`);
|
|
48
|
+
const info = await integration.ensureServe(cwd, serveEnv);
|
|
49
|
+
if (info) {
|
|
50
|
+
daemonLog(`agent-serve-manager: serve started for "${integration.id}", pid=${info.pid}, port=${info.port}`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
daemonLog(`agent-serve-manager: serve not started for "${integration.id}" (not supported or failed)`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
daemonLog(`agent-serve-manager: failed to start serve for "${integration.id}": ${String(err)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Gracefully shut down all managed serve processes.
|
|
63
|
+
* Called on daemon shutdown.
|
|
64
|
+
*/
|
|
65
|
+
async shutdownAll() {
|
|
66
|
+
for (const integration of this.integrations) {
|
|
67
|
+
if (!integration.shutdownServe)
|
|
68
|
+
continue;
|
|
69
|
+
try {
|
|
70
|
+
daemonLog(`agent-serve-manager: shutting down serve for "${integration.id}"`);
|
|
71
|
+
await integration.shutdownServe();
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
daemonLog(`agent-serve-manager: error shutting down "${integration.id}": ${String(err)}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -11,6 +11,10 @@ import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
|
11
11
|
import { OTLPMetricExporter as OTLPMetricExporterGrpc } from "@opentelemetry/exporter-metrics-otlp-grpc";
|
|
12
12
|
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
13
13
|
import { trace, context, propagation, metrics, SpanStatusCode, SpanKind } from "@opentelemetry/api";
|
|
14
|
+
import { logs } from "@opentelemetry/api-logs";
|
|
15
|
+
import { LoggerProvider, SimpleLogRecordProcessor } from "@opentelemetry/sdk-logs";
|
|
16
|
+
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
|
|
17
|
+
import { OTLPLogExporter as OTLPLogExporterGrpc } from "@opentelemetry/exporter-logs-otlp-grpc";
|
|
14
18
|
import { daemonLog } from "../daemon-log.js";
|
|
15
19
|
class OtelSpan {
|
|
16
20
|
constructor(span, ctx, onEnd) {
|
|
@@ -79,9 +83,11 @@ export class OpenTelemetryAdapter {
|
|
|
79
83
|
constructor(settings) {
|
|
80
84
|
this.sdk = null;
|
|
81
85
|
this.tracerProvider = null;
|
|
86
|
+
this.loggerProvider = null;
|
|
82
87
|
this.settings = settings;
|
|
83
88
|
this.tracer = trace.getTracer("loop-task");
|
|
84
89
|
this.meter = metrics.getMeter("loop-task");
|
|
90
|
+
this.logger = logs.getLogger("loop-task");
|
|
85
91
|
this.status = {
|
|
86
92
|
enabled: settings.telemetryEnabled,
|
|
87
93
|
exporterConfigured: !!settings.telemetryEndpoint,
|
|
@@ -93,7 +99,12 @@ export class OpenTelemetryAdapter {
|
|
|
93
99
|
captureCommandOutput: settings.telemetryCaptureCommandOutput,
|
|
94
100
|
exporterState: this.resolveExporterState(settings),
|
|
95
101
|
};
|
|
96
|
-
|
|
102
|
+
if (settings.telemetryEnabled && settings.telemetryEndpoint) {
|
|
103
|
+
this.initializeSdk(settings);
|
|
104
|
+
}
|
|
105
|
+
// Initialize metric instruments AFTER SDK startup so they bind to the
|
|
106
|
+
// real MeterProvider, not the default no-op one. When the SDK is not
|
|
107
|
+
// initialized, these still work — they bind to the API-level no-op meter.
|
|
97
108
|
this.runCounter = this.meter.createCounter(METRIC_NAMES.RUNS);
|
|
98
109
|
this.runDurationHistogram = this.meter.createHistogram(METRIC_NAMES.RUN_DURATION);
|
|
99
110
|
this.taskCounter = this.meter.createCounter(METRIC_NAMES.TASKS);
|
|
@@ -109,9 +120,6 @@ export class OpenTelemetryAdapter {
|
|
|
109
120
|
this.agentCacheWriteTokensCounter = this.meter.createCounter(METRIC_NAMES.AGENT_CACHE_WRITE_TOKENS);
|
|
110
121
|
this.agentCostCounter = this.meter.createCounter(METRIC_NAMES.AGENT_COST);
|
|
111
122
|
this.failureCounter = this.meter.createCounter(METRIC_NAMES.FAILURES);
|
|
112
|
-
if (settings.telemetryEnabled && settings.telemetryEndpoint) {
|
|
113
|
-
this.initializeSdk(settings);
|
|
114
|
-
}
|
|
115
123
|
}
|
|
116
124
|
resolveExporterState(settings) {
|
|
117
125
|
if (!settings.telemetryEnabled)
|
|
@@ -162,7 +170,24 @@ export class OpenTelemetryAdapter {
|
|
|
162
170
|
instrumentations: [],
|
|
163
171
|
});
|
|
164
172
|
this.sdk.start();
|
|
173
|
+
// Re-obtain tracer and meter from the global API after SDK startup.
|
|
174
|
+
// The API now resolves to the real providers registered by NodeSDK.
|
|
165
175
|
this.tracer = trace.getTracer(settings.telemetryServiceName);
|
|
176
|
+
this.meter = metrics.getMeter(settings.telemetryServiceName);
|
|
177
|
+
// Initialize OTLP log exporter
|
|
178
|
+
const logsUrl = endpoint.endsWith("/v1/logs")
|
|
179
|
+
? endpoint
|
|
180
|
+
: `${endpoint}/v1/logs`;
|
|
181
|
+
const logExporter = isGrpc
|
|
182
|
+
? new OTLPLogExporterGrpc({ url: settings.telemetryEndpoint })
|
|
183
|
+
: new OTLPLogExporter({ url: logsUrl, headers });
|
|
184
|
+
const loggerProvider = new LoggerProvider({
|
|
185
|
+
resource,
|
|
186
|
+
processors: [new SimpleLogRecordProcessor({ exporter: logExporter })],
|
|
187
|
+
});
|
|
188
|
+
logs.setGlobalLoggerProvider(loggerProvider);
|
|
189
|
+
this.loggerProvider = loggerProvider;
|
|
190
|
+
this.logger = logs.getLogger(settings.telemetryServiceName);
|
|
166
191
|
this.status.exporterState = "configured";
|
|
167
192
|
daemonLog(`telemetry: SDK initialized, endpoint=${settings.telemetryEndpoint}`);
|
|
168
193
|
}
|
|
@@ -197,6 +222,24 @@ export class OpenTelemetryAdapter {
|
|
|
197
222
|
return "http/protobuf";
|
|
198
223
|
return this.settings.telemetryProtocol;
|
|
199
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Check if any agent serve sidecar is alive.
|
|
227
|
+
* When serve is alive, static OTEL config lives in the serve process
|
|
228
|
+
* and we skip per-task injection.
|
|
229
|
+
*/
|
|
230
|
+
checkServeAlive() {
|
|
231
|
+
try {
|
|
232
|
+
const integrations = getAgentIntegrations();
|
|
233
|
+
for (const integration of integrations) {
|
|
234
|
+
if (integration.isServeAlive?.())
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
200
243
|
startLoop(input) {
|
|
201
244
|
const span = this.tracer.startSpan(SPAN_NAMES.LOOP_RUN, { kind: SpanKind.SERVER });
|
|
202
245
|
span.setAttributes({
|
|
@@ -324,6 +367,20 @@ export class OpenTelemetryAdapter {
|
|
|
324
367
|
activeSpan.setAttribute("gen_ai.request.model", input.model);
|
|
325
368
|
}
|
|
326
369
|
}
|
|
370
|
+
logEvent(level, message, attributes) {
|
|
371
|
+
try {
|
|
372
|
+
const record = this.logger.emit({
|
|
373
|
+
body: message,
|
|
374
|
+
severityText: level,
|
|
375
|
+
attributes: attributes ?? {},
|
|
376
|
+
});
|
|
377
|
+
// Ensure the log record is flushed in a timely manner
|
|
378
|
+
void record;
|
|
379
|
+
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
daemonLog(`telemetry: log emit failed: ${String(err)}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
327
384
|
prepareChildProcess(invocation, childContext, integrationOverride) {
|
|
328
385
|
const env = {};
|
|
329
386
|
const endpoint = this.resolveEndpoint();
|
|
@@ -337,25 +394,34 @@ export class OpenTelemetryAdapter {
|
|
|
337
394
|
env.TRACESTATE = childContext.traceState;
|
|
338
395
|
}
|
|
339
396
|
}
|
|
340
|
-
|
|
341
|
-
env
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
env.
|
|
397
|
+
// Check if an agent serve sidecar is alive — if so, static OTEL config
|
|
398
|
+
// lives in the serve process env and we skip per-task injection.
|
|
399
|
+
const serveAlive = this.checkServeAlive();
|
|
400
|
+
if (!serveAlive) {
|
|
401
|
+
env.OTEL_EXPORTER_OTLP_ENDPOINT = endpoint;
|
|
402
|
+
env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = endpoint;
|
|
403
|
+
env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = endpoint;
|
|
404
|
+
env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = endpoint;
|
|
405
|
+
env.OTEL_EXPORTER_OTLP_PROTOCOL = protocol;
|
|
406
|
+
env.OTEL_TRACES_EXPORTER = "otlp";
|
|
407
|
+
env.OTEL_METRICS_EXPORTER = "otlp";
|
|
408
|
+
env.OTEL_LOGS_EXPORTER = "otlp";
|
|
409
|
+
const authHeaders = process.env.OTEL_EXPORTER_OTLP_HEADERS
|
|
410
|
+
?? process.env.OTEL_EXPORTER_OTLP_TRACES_HEADERS;
|
|
411
|
+
if (authHeaders) {
|
|
412
|
+
env.OTEL_EXPORTER_OTLP_HEADERS = authHeaders;
|
|
413
|
+
}
|
|
414
|
+
const correlationAttrs = {
|
|
415
|
+
[CORRELATION_KEYS.RUN_ID]: childContext.runId,
|
|
416
|
+
[CORRELATION_KEYS.LOOP_ID]: childContext.loopId,
|
|
417
|
+
};
|
|
418
|
+
if (childContext.taskId)
|
|
419
|
+
correlationAttrs[CORRELATION_KEYS.TASK_ID] = childContext.taskId;
|
|
420
|
+
if (childContext.projectId)
|
|
421
|
+
correlationAttrs[CORRELATION_KEYS.PROJECT_ID] = childContext.projectId;
|
|
422
|
+
const mergedResourceAttrs = this.mergeResourceAttributes(process.env.OTEL_RESOURCE_ATTRIBUTES, correlationAttrs);
|
|
423
|
+
env.OTEL_RESOURCE_ATTRIBUTES = mergedResourceAttrs;
|
|
348
424
|
}
|
|
349
|
-
const correlationAttrs = {
|
|
350
|
-
[CORRELATION_KEYS.RUN_ID]: childContext.runId,
|
|
351
|
-
[CORRELATION_KEYS.LOOP_ID]: childContext.loopId,
|
|
352
|
-
};
|
|
353
|
-
if (childContext.taskId)
|
|
354
|
-
correlationAttrs[CORRELATION_KEYS.TASK_ID] = childContext.taskId;
|
|
355
|
-
if (childContext.projectId)
|
|
356
|
-
correlationAttrs[CORRELATION_KEYS.PROJECT_ID] = childContext.projectId;
|
|
357
|
-
const mergedResourceAttrs = this.mergeResourceAttributes(process.env.OTEL_RESOURCE_ATTRIBUTES, correlationAttrs);
|
|
358
|
-
env.OTEL_RESOURCE_ATTRIBUTES = mergedResourceAttrs;
|
|
359
425
|
// Apply agent-specific integrations when auto-instrumentation is enabled
|
|
360
426
|
let integrationId;
|
|
361
427
|
if (this.settings.telemetryAutoInstrumentAgents || integrationOverride) {
|
|
@@ -419,6 +485,9 @@ export class OpenTelemetryAdapter {
|
|
|
419
485
|
if (this.tracerProvider) {
|
|
420
486
|
flushes.push(this.tracerProvider.forceFlush());
|
|
421
487
|
}
|
|
488
|
+
if (this.loggerProvider) {
|
|
489
|
+
flushes.push(this.loggerProvider.forceFlush());
|
|
490
|
+
}
|
|
422
491
|
const sdkInternals = this.sdk;
|
|
423
492
|
if (typeof sdkInternals._meterProvider?.forceFlush === "function") {
|
|
424
493
|
flushes.push(sdkInternals._meterProvider.forceFlush());
|
|
@@ -450,6 +519,16 @@ export class OpenTelemetryAdapter {
|
|
|
450
519
|
}
|
|
451
520
|
this.tracerProvider = null;
|
|
452
521
|
}
|
|
522
|
+
if (this.loggerProvider) {
|
|
523
|
+
try {
|
|
524
|
+
await this.loggerProvider.forceFlush();
|
|
525
|
+
await this.loggerProvider.shutdown();
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
// best effort
|
|
529
|
+
}
|
|
530
|
+
this.loggerProvider = null;
|
|
531
|
+
}
|
|
453
532
|
if (!this.sdk)
|
|
454
533
|
return;
|
|
455
534
|
try {
|
package/dist/entry.js
CHANGED
|
File without changes
|
package/dist/shared/i18n/en.json
CHANGED
|
@@ -460,7 +460,7 @@
|
|
|
460
460
|
"project.wizard.directoryPrompt": "Working directory? (optional)",
|
|
461
461
|
"project.wizard.directoryHint": "Default directory for loops in this project. Leave blank to inherit",
|
|
462
462
|
"project.wizard.githubSourcePrompt": "GitHub Source? (optional)",
|
|
463
|
-
"project.wizard.githubSourceHint": "Repository in owner/repo format, e.g.
|
|
463
|
+
"project.wizard.githubSourceHint": "Repository in owner/repo format, e.g. PlainConceptsPlatform/loop-task",
|
|
464
464
|
"project.error.updateFailed": "Failed to update project",
|
|
465
465
|
"project.error.deleteFailed": "Failed to delete project",
|
|
466
466
|
"project.toastCreated": "Project \"{name}\" created",
|