@desplega.ai/agent-swarm 1.79.4 → 1.80.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/openapi.json +496 -32
- package/package.json +14 -6
- package/src/artifact-sdk/server.ts +2 -1
- package/src/be/db.ts +102 -31
- package/src/be/migrations/063_cost_context_schema_relax.sql +133 -0
- package/src/be/migrations/064_scripts.sql +39 -0
- package/src/be/migrations/065_script_embeddings.sql +7 -0
- package/src/be/pricing-normalize.ts +81 -0
- package/src/be/scripts/db.ts +391 -0
- package/src/be/scripts/embeddings.ts +231 -0
- package/src/be/scripts/maintenance.ts +9 -0
- package/src/be/scripts/typecheck.ts +193 -0
- package/src/be/seed-pricing.ts +293 -0
- package/src/cli.tsx +22 -5
- package/src/commands/artifact.ts +3 -2
- package/src/commands/claude-managed-setup.ts +21 -4
- package/src/commands/codex-login.ts +5 -3
- package/src/commands/onboard.tsx +2 -1
- package/src/commands/runner.ts +663 -246
- package/src/commands/setup.tsx +5 -3
- package/src/hooks/hook.ts +4 -3
- package/src/http/context.ts +6 -2
- package/src/http/index.ts +126 -68
- package/src/http/memory.ts +28 -0
- package/src/http/openapi.ts +1 -0
- package/src/http/page-proxy.ts +2 -1
- package/src/http/route-def.ts +1 -0
- package/src/http/schedules.ts +37 -0
- package/src/http/scripts.ts +381 -0
- package/src/http/session-data.ts +74 -23
- package/src/linear/outbound.ts +9 -2
- package/src/otel-impl.ts +200 -0
- package/src/otel.ts +132 -0
- package/src/providers/claude-adapter.ts +52 -6
- package/src/providers/claude-managed-adapter.ts +43 -17
- package/src/providers/claude-managed-pricing.ts +34 -0
- package/src/providers/codex-adapter.ts +38 -27
- package/src/providers/codex-models.ts +22 -3
- package/src/providers/devin-adapter.ts +11 -0
- package/src/providers/opencode-adapter.ts +31 -7
- package/src/providers/pi-mono-adapter.ts +39 -7
- package/src/providers/pricing-sources.md +52 -0
- package/src/providers/swarm-events-shared.ts +8 -4
- package/src/providers/types.ts +33 -10
- package/src/scripts-runtime/ctx.ts +23 -0
- package/src/scripts-runtime/eval-harness.ts +39 -0
- package/src/scripts-runtime/executors/native.ts +229 -0
- package/src/scripts-runtime/executors/registry.ts +16 -0
- package/src/scripts-runtime/executors/types.ts +63 -0
- package/src/scripts-runtime/extract-signature.ts +81 -0
- package/src/scripts-runtime/import-allowlist.ts +109 -0
- package/src/scripts-runtime/loader.ts +96 -0
- package/src/scripts-runtime/redacted.ts +48 -0
- package/src/scripts-runtime/sdk-allowlist.ts +29 -0
- package/src/scripts-runtime/stdlib/fetch.ts +46 -0
- package/src/scripts-runtime/stdlib/glob.ts +8 -0
- package/src/scripts-runtime/stdlib/grep.ts +34 -0
- package/src/scripts-runtime/stdlib/index.ts +16 -0
- package/src/scripts-runtime/stdlib/table.ts +17 -0
- package/src/scripts-runtime/swarm-config.ts +35 -0
- package/src/scripts-runtime/swarm-sdk.ts +197 -0
- package/src/scripts-runtime/types/stdlib.d.ts +104 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +86 -0
- package/src/server.ts +18 -0
- package/src/tests/api-key.test.ts +33 -0
- package/src/tests/claude-managed-adapter.test.ts +17 -3
- package/src/tests/claude-managed-setup.test.ts +10 -1
- package/src/tests/codex-adapter.test.ts +20 -19
- package/src/tests/codex-login.test.ts +1 -1
- package/src/tests/context-snapshot.test.ts +2 -2
- package/src/tests/context-window.test.ts +65 -1
- package/src/tests/devin-adapter.test.ts +2 -0
- package/src/tests/http/context-routes.test.ts +161 -0
- package/src/tests/linear-outbound-sync.test.ts +109 -0
- package/src/tests/mcp-tools.test.ts +69 -0
- package/src/tests/migration-063-schema-relax.test.ts +109 -0
- package/src/tests/opencode-adapter.test.ts +146 -1
- package/src/tests/otel-impl-secret-scrubbing.test.ts +33 -0
- package/src/tests/pages-view-count.test.ts +30 -5
- package/src/tests/providers/codex-cost.test.ts +18 -0
- package/src/tests/providers/opencode-cost.test.ts +74 -0
- package/src/tests/providers/pi-cost.test.ts +128 -0
- package/src/tests/redacted.test.ts +29 -0
- package/src/tests/runner-tool-spans.test.ts +268 -0
- package/src/tests/script-executor-conformance.test.ts +142 -0
- package/src/tests/script-executor-registry.test.ts +17 -0
- package/src/tests/scripts-db.test.ts +329 -0
- package/src/tests/scripts-embeddings.test.ts +291 -0
- package/src/tests/scripts-extract-signature.test.ts +47 -0
- package/src/tests/scripts-http.test.ts +350 -0
- package/src/tests/scripts-import-allowlist.test.ts +55 -0
- package/src/tests/scripts-mcp-e2e.test.ts +269 -0
- package/src/tests/scripts-runtime-secret-egress.test.ts +44 -0
- package/src/tests/scripts-runtime.test.ts +289 -0
- package/src/tests/sdk-allowlist.test.ts +59 -0
- package/src/tests/secret-scrubber.test.ts +54 -1
- package/src/tests/session-costs-codex-recompute.test.ts +35 -22
- package/src/tests/session-costs-model-key-normalize.test.ts +271 -0
- package/src/tests/session-costs-recompute-all-providers.test.ts +170 -0
- package/src/tests/store-progress-cost.test.ts +6 -1
- package/src/tests/swarm-config.test.ts +38 -0
- package/src/tests/tool-annotations.test.ts +2 -2
- package/src/tests/tool-call-progress.test.ts +30 -0
- package/src/tests/workflow-e2e.test.ts +218 -0
- package/src/tests/workflow-executors.test.ts +32 -2
- package/src/tests/workflow-input-redaction.test.ts +232 -0
- package/src/tests/workflow-swarm-script.test.ts +273 -0
- package/src/tools/memory-rate.ts +2 -1
- package/src/tools/script-common.ts +88 -0
- package/src/tools/script-delete.ts +35 -0
- package/src/tools/script-query-types.ts +37 -0
- package/src/tools/script-run.ts +43 -0
- package/src/tools/script-search.ts +32 -0
- package/src/tools/script-upsert.ts +43 -0
- package/src/tools/store-progress.ts +16 -60
- package/src/tools/tool-config.ts +7 -0
- package/src/tools/utils.ts +65 -12
- package/src/types.ts +122 -10
- package/src/utils/api-key.ts +28 -0
- package/src/utils/context-window.ts +104 -4
- package/src/utils/page-session.ts +8 -6
- package/src/utils/secret-scrubber.ts +29 -1
- package/src/workflows/engine.ts +12 -4
- package/src/workflows/executors/index.ts +1 -0
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/executors/script.ts +12 -1
- package/src/workflows/executors/swarm-script.ts +170 -0
- package/src/workflows/input.ts +65 -0
- package/src/workflows/recovery.ts +31 -3
- package/src/workflows/resume.ts +43 -5
package/src/otel-impl.ts
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import {
|
|
2
|
+
context,
|
|
3
|
+
propagation,
|
|
4
|
+
ROOT_CONTEXT,
|
|
5
|
+
type Span,
|
|
6
|
+
SpanStatusCode,
|
|
7
|
+
trace,
|
|
8
|
+
} from "@opentelemetry/api";
|
|
9
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
10
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
11
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
12
|
+
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
|
|
13
|
+
import pkg from "../package.json";
|
|
14
|
+
import type { SwarmSpan } from "./otel";
|
|
15
|
+
import { scrubSecrets } from "./utils/secret-scrubber";
|
|
16
|
+
|
|
17
|
+
type AttributeValue = string | number | boolean | string[] | number[] | boolean[];
|
|
18
|
+
type Attributes = Record<string, AttributeValue | undefined>;
|
|
19
|
+
|
|
20
|
+
const TRACER_NAME = "agent-swarm";
|
|
21
|
+
const RAW_SPAN = Symbol("agent-swarm.raw-span");
|
|
22
|
+
|
|
23
|
+
let sdk: NodeSDK | undefined;
|
|
24
|
+
|
|
25
|
+
function decodeResourceAttributeValue(value: string): string {
|
|
26
|
+
try {
|
|
27
|
+
return decodeURIComponent(value);
|
|
28
|
+
} catch {
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseResourceAttributes(value = process.env.OTEL_RESOURCE_ATTRIBUTES): Attributes {
|
|
34
|
+
if (!value) return {};
|
|
35
|
+
const attributes: Attributes = {};
|
|
36
|
+
for (const pair of value.split(",")) {
|
|
37
|
+
const [rawKey, ...rawValueParts] = pair.split("=");
|
|
38
|
+
const key = rawKey?.trim();
|
|
39
|
+
if (!key) continue;
|
|
40
|
+
const rawValue = rawValueParts.join("=").trim();
|
|
41
|
+
if (!rawValue) continue;
|
|
42
|
+
attributes[key] = decodeResourceAttributeValue(rawValue);
|
|
43
|
+
}
|
|
44
|
+
return attributes;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function cleanAttributes(attributes?: Attributes): Record<string, AttributeValue> | undefined {
|
|
48
|
+
if (!attributes) return undefined;
|
|
49
|
+
const cleaned: Record<string, AttributeValue> = {};
|
|
50
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
51
|
+
if (value !== undefined) cleaned[key] = value;
|
|
52
|
+
}
|
|
53
|
+
return cleaned;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function scrubOtelException(error: unknown): Error | string {
|
|
57
|
+
if (!(error instanceof Error)) {
|
|
58
|
+
return scrubSecrets(String(error));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const scrubbed = new Error(scrubSecrets(error.message));
|
|
62
|
+
scrubbed.name = error.name;
|
|
63
|
+
if (error.stack) {
|
|
64
|
+
scrubbed.stack = scrubSecrets(error.stack);
|
|
65
|
+
}
|
|
66
|
+
return scrubbed;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function scrubOtelStatus(status: { code: number; message?: string }) {
|
|
70
|
+
return status.message === undefined
|
|
71
|
+
? status
|
|
72
|
+
: {
|
|
73
|
+
...status,
|
|
74
|
+
message: scrubSecrets(status.message),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
type AdaptedSwarmSpan = SwarmSpan & { [RAW_SPAN]: Span };
|
|
79
|
+
|
|
80
|
+
function spanAdapter(span: Span): AdaptedSwarmSpan {
|
|
81
|
+
return {
|
|
82
|
+
[RAW_SPAN]: span,
|
|
83
|
+
setAttribute(key, value) {
|
|
84
|
+
span.setAttribute(key, value);
|
|
85
|
+
return this;
|
|
86
|
+
},
|
|
87
|
+
setAttributes(attributes) {
|
|
88
|
+
const cleaned = cleanAttributes(attributes);
|
|
89
|
+
if (cleaned) span.setAttributes(cleaned);
|
|
90
|
+
return this;
|
|
91
|
+
},
|
|
92
|
+
addEvent(name, attributes) {
|
|
93
|
+
const cleaned = cleanAttributes(attributes);
|
|
94
|
+
span.addEvent(name, cleaned);
|
|
95
|
+
return this;
|
|
96
|
+
},
|
|
97
|
+
recordException(error) {
|
|
98
|
+
span.recordException(scrubOtelException(error));
|
|
99
|
+
},
|
|
100
|
+
setStatus(status) {
|
|
101
|
+
span.setStatus(scrubOtelStatus(status));
|
|
102
|
+
return this;
|
|
103
|
+
},
|
|
104
|
+
end() {
|
|
105
|
+
span.end();
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function boot(serviceRole: string): Promise<void> {
|
|
111
|
+
if (sdk) return;
|
|
112
|
+
|
|
113
|
+
const configuredResourceAttributes = parseResourceAttributes();
|
|
114
|
+
const deploymentEnvironment =
|
|
115
|
+
configuredResourceAttributes["deployment.environment"] || process.env.NODE_ENV || "development";
|
|
116
|
+
const serviceName =
|
|
117
|
+
process.env.OTEL_SERVICE_NAME ||
|
|
118
|
+
(serviceRole === "api" ? "agent-swarm-api" : "agent-swarm-worker");
|
|
119
|
+
sdk = new NodeSDK({
|
|
120
|
+
resource: resourceFromAttributes({
|
|
121
|
+
...configuredResourceAttributes,
|
|
122
|
+
[ATTR_SERVICE_NAME]: serviceName,
|
|
123
|
+
[ATTR_SERVICE_VERSION]: pkg.version,
|
|
124
|
+
"service.namespace": configuredResourceAttributes["service.namespace"] || "agent-swarm",
|
|
125
|
+
"service.instance.id": process.env.AGENT_ID || crypto.randomUUID(),
|
|
126
|
+
"deployment.environment": deploymentEnvironment,
|
|
127
|
+
env: configuredResourceAttributes.env || deploymentEnvironment,
|
|
128
|
+
"agentswarm.service.role": serviceRole,
|
|
129
|
+
}),
|
|
130
|
+
traceExporter: new OTLPTraceExporter(),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
sdk.start();
|
|
134
|
+
|
|
135
|
+
const shutdown = async () => {
|
|
136
|
+
try {
|
|
137
|
+
await sdk?.shutdown();
|
|
138
|
+
} catch {
|
|
139
|
+
// Best-effort flush during process shutdown.
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
process.once("SIGTERM", shutdown);
|
|
144
|
+
process.once("SIGINT", shutdown);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function shutdown(): Promise<void> {
|
|
148
|
+
await sdk?.shutdown();
|
|
149
|
+
sdk = undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function withSpan<T>(
|
|
153
|
+
name: string,
|
|
154
|
+
fn: (span: SwarmSpan) => Promise<T> | T,
|
|
155
|
+
attributes?: Attributes,
|
|
156
|
+
): Promise<T> {
|
|
157
|
+
const tracer = trace.getTracer(TRACER_NAME);
|
|
158
|
+
return tracer.startActiveSpan(name, { attributes: cleanAttributes(attributes) }, async (span) => {
|
|
159
|
+
try {
|
|
160
|
+
const result = await fn(spanAdapter(span));
|
|
161
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
162
|
+
return result;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
span.recordException(scrubOtelException(error));
|
|
165
|
+
span.setStatus({
|
|
166
|
+
code: SpanStatusCode.ERROR,
|
|
167
|
+
message: scrubSecrets(error instanceof Error ? error.message : String(error)),
|
|
168
|
+
});
|
|
169
|
+
throw error;
|
|
170
|
+
} finally {
|
|
171
|
+
span.end();
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function startSpan(name: string, attributes?: Attributes): SwarmSpan {
|
|
177
|
+
const span = trace.getTracer(TRACER_NAME).startSpan(name, {
|
|
178
|
+
attributes: cleanAttributes(attributes),
|
|
179
|
+
});
|
|
180
|
+
return spanAdapter(span);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function withSpanContext<T>(span: SwarmSpan, fn: () => T): T {
|
|
184
|
+
const rawSpan = (span as Partial<AdaptedSwarmSpan>)[RAW_SPAN];
|
|
185
|
+
if (!rawSpan) return fn();
|
|
186
|
+
return context.with(trace.setSpan(context.active(), rawSpan), fn);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function withRemoteContext<T>(
|
|
190
|
+
carrier: Record<string, unknown>,
|
|
191
|
+
fn: () => Promise<T> | T,
|
|
192
|
+
): Promise<T> {
|
|
193
|
+
const remoteContext = propagation.extract(ROOT_CONTEXT, carrier);
|
|
194
|
+
return context.with(remoteContext, fn);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function injectTraceContext(headers: Record<string, string>): Record<string, string> {
|
|
198
|
+
propagation.inject(context.active(), headers);
|
|
199
|
+
return headers;
|
|
200
|
+
}
|
package/src/otel.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
export type AttributeValue = string | number | boolean | string[] | number[] | boolean[];
|
|
2
|
+
export type Attributes = Record<string, AttributeValue | undefined>;
|
|
3
|
+
|
|
4
|
+
type SpanStatus = {
|
|
5
|
+
code: number;
|
|
6
|
+
message?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type SwarmSpan = {
|
|
10
|
+
setAttribute: (key: string, value: AttributeValue) => SwarmSpan;
|
|
11
|
+
setAttributes: (attributes: Attributes) => SwarmSpan;
|
|
12
|
+
addEvent: (name: string, attributes?: Attributes) => SwarmSpan;
|
|
13
|
+
recordException: (error: unknown) => void;
|
|
14
|
+
setStatus: (status: SpanStatus) => SwarmSpan;
|
|
15
|
+
end: () => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const enabled = Boolean(process.env.OTEL_EXPORTER_OTLP_ENDPOINT);
|
|
19
|
+
|
|
20
|
+
const NOOP_SPAN: SwarmSpan = {
|
|
21
|
+
setAttribute: () => NOOP_SPAN,
|
|
22
|
+
setAttributes: () => NOOP_SPAN,
|
|
23
|
+
addEvent: () => NOOP_SPAN,
|
|
24
|
+
recordException: () => {},
|
|
25
|
+
setStatus: () => NOOP_SPAN,
|
|
26
|
+
end: () => {},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
let initialized = false;
|
|
30
|
+
let realWithSpan:
|
|
31
|
+
| (<T>(
|
|
32
|
+
name: string,
|
|
33
|
+
fn: (span: SwarmSpan) => Promise<T> | T,
|
|
34
|
+
attributes?: Attributes,
|
|
35
|
+
) => Promise<T>)
|
|
36
|
+
| undefined;
|
|
37
|
+
let realStartSpan: ((name: string, attributes?: Attributes) => SwarmSpan) | undefined;
|
|
38
|
+
let realWithRemoteContext:
|
|
39
|
+
| (<T>(carrier: Record<string, unknown>, fn: () => Promise<T> | T) => Promise<T>)
|
|
40
|
+
| undefined;
|
|
41
|
+
let realWithSpanContext: (<T>(span: SwarmSpan, fn: () => T) => T) | undefined;
|
|
42
|
+
let realInjectTraceContext:
|
|
43
|
+
| ((headers: Record<string, string>) => Record<string, string>)
|
|
44
|
+
| undefined;
|
|
45
|
+
let realShutdown: (() => Promise<void>) | undefined;
|
|
46
|
+
|
|
47
|
+
export function isOtelEnabled(): boolean {
|
|
48
|
+
return enabled;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function isPollTracingEnabled(): boolean {
|
|
52
|
+
const v = (process.env.OTEL_TRACE_POLL ?? "").toLowerCase();
|
|
53
|
+
return v === "1" || v === "true" || v === "yes" || v === "on";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function initOtel(serviceRole = process.env.AGENT_ROLE || "api"): Promise<void> {
|
|
57
|
+
if (!enabled || initialized) return;
|
|
58
|
+
initialized = true;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const impl = await import("./otel-impl");
|
|
62
|
+
await impl.boot(serviceRole);
|
|
63
|
+
realWithSpan = impl.withSpan;
|
|
64
|
+
realStartSpan = impl.startSpan;
|
|
65
|
+
realWithRemoteContext = impl.withRemoteContext;
|
|
66
|
+
realWithSpanContext = impl.withSpanContext;
|
|
67
|
+
realInjectTraceContext = impl.injectTraceContext;
|
|
68
|
+
realShutdown = impl.shutdown;
|
|
69
|
+
console.log(
|
|
70
|
+
`[OTel] enabled for ${process.env.OTEL_SERVICE_NAME ?? "agent-swarm"} (${serviceRole})`,
|
|
71
|
+
);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.warn(`[OTel] disabled after initialization failure: ${error}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function withSpan<T>(
|
|
78
|
+
name: string,
|
|
79
|
+
fn: (span: SwarmSpan) => Promise<T> | T,
|
|
80
|
+
attributes?: Attributes,
|
|
81
|
+
): Promise<T> {
|
|
82
|
+
if (!enabled || !realWithSpan) {
|
|
83
|
+
return fn(NOOP_SPAN);
|
|
84
|
+
}
|
|
85
|
+
return realWithSpan(name, fn, attributes);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function startSpan(name: string, attributes?: Attributes): SwarmSpan {
|
|
89
|
+
if (!enabled || !realStartSpan) {
|
|
90
|
+
return NOOP_SPAN;
|
|
91
|
+
}
|
|
92
|
+
return realStartSpan(name, attributes);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function withSpanContext<T>(span: SwarmSpan, fn: () => T): T {
|
|
96
|
+
if (!enabled || !realWithSpanContext) {
|
|
97
|
+
return fn();
|
|
98
|
+
}
|
|
99
|
+
return realWithSpanContext(span, fn);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function withRemoteContext<T>(
|
|
103
|
+
carrier: Record<string, unknown>,
|
|
104
|
+
fn: () => Promise<T> | T,
|
|
105
|
+
): Promise<T> {
|
|
106
|
+
if (!enabled || !realWithRemoteContext) {
|
|
107
|
+
return fn();
|
|
108
|
+
}
|
|
109
|
+
return realWithRemoteContext(carrier, fn);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function injectTraceContext(headers: Record<string, string>): Record<string, string> {
|
|
113
|
+
if (!enabled || !realInjectTraceContext) {
|
|
114
|
+
return headers;
|
|
115
|
+
}
|
|
116
|
+
return realInjectTraceContext(headers);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function shutdownOtel(): Promise<void> {
|
|
120
|
+
if (!realShutdown) return;
|
|
121
|
+
await realShutdown();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function _resetOtelForTests() {
|
|
125
|
+
initialized = false;
|
|
126
|
+
realWithSpan = undefined;
|
|
127
|
+
realStartSpan = undefined;
|
|
128
|
+
realWithRemoteContext = undefined;
|
|
129
|
+
realWithSpanContext = undefined;
|
|
130
|
+
realInjectTraceContext = undefined;
|
|
131
|
+
realShutdown = undefined;
|
|
132
|
+
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { readFile, unlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
CONTEXT_FORMULA,
|
|
6
|
+
clampContextPercent,
|
|
7
|
+
computeContextUsedUnified,
|
|
8
|
+
getContextWindowSize,
|
|
9
|
+
} from "../utils/context-window";
|
|
5
10
|
import { validateClaudeCredentials } from "../utils/credentials";
|
|
6
11
|
import {
|
|
7
12
|
parseStderrForErrors,
|
|
@@ -465,6 +470,10 @@ class ClaudeSession implements ProviderSession {
|
|
|
465
470
|
this._sessionId = json.session_id;
|
|
466
471
|
this.emit({ type: "session_init", sessionId: json.session_id, provider: "claude" });
|
|
467
472
|
if (json.model) {
|
|
473
|
+
// Phase 4: the CLI's `init.model` reflects the actual model after any
|
|
474
|
+
// backoff/fallback. Update `this.model` so subsequent CostData rows
|
|
475
|
+
// (and the pricing lookup the API runs) use the right rate.
|
|
476
|
+
this.model = json.model;
|
|
468
477
|
this.contextWindowSize = getContextWindowSize(json.model);
|
|
469
478
|
}
|
|
470
479
|
}
|
|
@@ -487,6 +496,10 @@ class ClaudeSession implements ProviderSession {
|
|
|
487
496
|
output_tokens?: number;
|
|
488
497
|
cache_read_input_tokens?: number;
|
|
489
498
|
cache_creation_input_tokens?: number;
|
|
499
|
+
// Phase 4: claude extended-thinking flows surface this — the
|
|
500
|
+
// CLI emits `thinking_input_tokens` when the model produced
|
|
501
|
+
// thinking content during the turn.
|
|
502
|
+
thinking_input_tokens?: number;
|
|
490
503
|
}
|
|
491
504
|
| undefined;
|
|
492
505
|
|
|
@@ -499,8 +512,12 @@ class ClaudeSession implements ProviderSession {
|
|
|
499
512
|
outputTokens: usage?.output_tokens ?? 0,
|
|
500
513
|
cacheReadTokens: usage?.cache_read_input_tokens ?? 0,
|
|
501
514
|
cacheWriteTokens: usage?.cache_creation_input_tokens ?? 0,
|
|
515
|
+
// Phase 4: surface thinking tokens; previously dropped on the floor.
|
|
516
|
+
thinkingTokens: usage?.thinking_input_tokens ?? 0,
|
|
502
517
|
durationMs: json.duration_ms || 0,
|
|
503
|
-
|
|
518
|
+
// Phase 4: honest null when the CLI omits num_turns instead of a
|
|
519
|
+
// faked `1` (would have under-counted in dashboards).
|
|
520
|
+
numTurns: json.num_turns ?? null,
|
|
504
521
|
model: this.model,
|
|
505
522
|
isError: json.is_error || false,
|
|
506
523
|
provider: "claude",
|
|
@@ -524,8 +541,29 @@ class ClaudeSession implements ProviderSession {
|
|
|
524
541
|
// Tool use from assistant messages — emit tool_start for auto-progress
|
|
525
542
|
if (json.type === "assistant" && json.message) {
|
|
526
543
|
const message = json.message as {
|
|
527
|
-
content?: Array<{
|
|
544
|
+
content?: Array<{
|
|
545
|
+
type: string;
|
|
546
|
+
name?: string;
|
|
547
|
+
id?: string;
|
|
548
|
+
input?: unknown;
|
|
549
|
+
text?: string;
|
|
550
|
+
}>;
|
|
528
551
|
};
|
|
552
|
+
|
|
553
|
+
// Emit a `message` event BEFORE any tool_start events for this turn.
|
|
554
|
+
// The runner uses this as an "assistant turn boundary" to implicit-close
|
|
555
|
+
// any worker.tool spans left open by the previous turn (the Claude CLI
|
|
556
|
+
// doesn't emit per-tool completion events for harness-side tools like
|
|
557
|
+
// Bash/Read/Edit, so without this boundary their spans would stay open
|
|
558
|
+
// until session shutdown and report inflated duration_ms).
|
|
559
|
+
const text = Array.isArray(message.content)
|
|
560
|
+
? message.content
|
|
561
|
+
.filter((b) => b.type === "text" && typeof b.text === "string")
|
|
562
|
+
.map((b) => b.text as string)
|
|
563
|
+
.join("")
|
|
564
|
+
: "";
|
|
565
|
+
this.emit({ type: "message", role: "assistant", content: text });
|
|
566
|
+
|
|
529
567
|
if (message.content) {
|
|
530
568
|
for (const block of message.content) {
|
|
531
569
|
if (block.type === "tool_use" && block.name) {
|
|
@@ -539,18 +577,26 @@ class ClaudeSession implements ProviderSession {
|
|
|
539
577
|
}
|
|
540
578
|
}
|
|
541
579
|
|
|
542
|
-
// Context usage extraction from assistant message usage
|
|
580
|
+
// Context usage extraction from assistant message usage.
|
|
581
|
+
// Phase 9: unified `input + cache + output` formula across every
|
|
582
|
+
// provider so cross-provider percent comparisons are meaningful.
|
|
543
583
|
if (json.message.usage) {
|
|
544
584
|
const usage = json.message.usage;
|
|
545
|
-
const contextUsed =
|
|
585
|
+
const contextUsed = computeContextUsedUnified({
|
|
586
|
+
inputTokens: usage.input_tokens,
|
|
587
|
+
cacheReadTokens: usage.cache_read_input_tokens,
|
|
588
|
+
cacheCreateTokens: usage.cache_creation_input_tokens,
|
|
589
|
+
outputTokens: usage.output_tokens,
|
|
590
|
+
});
|
|
546
591
|
const contextTotal = this.contextWindowSize;
|
|
547
592
|
|
|
548
593
|
this.emit({
|
|
549
594
|
type: "context_usage",
|
|
550
595
|
contextUsedTokens: contextUsed,
|
|
551
596
|
contextTotalTokens: contextTotal,
|
|
552
|
-
contextPercent:
|
|
597
|
+
contextPercent: clampContextPercent(contextUsed, contextTotal) ?? 0,
|
|
553
598
|
outputTokens: usage.output_tokens ?? 0,
|
|
599
|
+
contextFormula: CONTEXT_FORMULA,
|
|
554
600
|
});
|
|
555
601
|
}
|
|
556
602
|
}
|
|
@@ -59,8 +59,15 @@ import type {
|
|
|
59
59
|
import type { SkillCreateResponse as Skill } from "@anthropic-ai/sdk/resources/beta/skills";
|
|
60
60
|
|
|
61
61
|
import { checkToolLoop } from "../hooks/tool-loop-detection";
|
|
62
|
+
import {
|
|
63
|
+
CONTEXT_FORMULA,
|
|
64
|
+
clampContextPercent,
|
|
65
|
+
computeContextUsedUnified,
|
|
66
|
+
getContextWindowSize,
|
|
67
|
+
} from "../utils/context-window";
|
|
62
68
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
63
69
|
import { computeClaudeManagedCostUsd } from "./claude-managed-models";
|
|
70
|
+
import { getRuntimeFeePerHour } from "./claude-managed-pricing";
|
|
64
71
|
import { createClaudeManagedSwarmEventHandler } from "./claude-managed-swarm-events";
|
|
65
72
|
import type {
|
|
66
73
|
CostData,
|
|
@@ -113,13 +120,10 @@ const REQUIRED_ENV_VARS = [
|
|
|
113
120
|
"MANAGED_ENVIRONMENT_ID",
|
|
114
121
|
] as const;
|
|
115
122
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
* per-model lookup.
|
|
121
|
-
*/
|
|
122
|
-
const DEFAULT_CONTEXT_TOTAL_TOKENS = 1_000_000;
|
|
123
|
+
// Phase 5: removed the hardcoded `DEFAULT_CONTEXT_TOTAL_TOKENS = 1_000_000`.
|
|
124
|
+
// The adapter now calls `getContextWindowSize(this.model)` from
|
|
125
|
+
// `src/utils/context-window.ts`, which resolves shortnames + dated full ids
|
|
126
|
+
// so haiku-4-5 sessions don't pretend to have a 1M window.
|
|
123
127
|
|
|
124
128
|
/**
|
|
125
129
|
* Compose the per-session user-message content blocks. Returns two blocks:
|
|
@@ -187,6 +191,8 @@ function emptyCost(config: ProviderSessionConfig, model: string): CostData {
|
|
|
187
191
|
numTurns: 0,
|
|
188
192
|
model,
|
|
189
193
|
isError: false,
|
|
194
|
+
// Phase 3 — tag every emitted CostData so the API's recompute path engages.
|
|
195
|
+
provider: "claude-managed",
|
|
190
196
|
};
|
|
191
197
|
}
|
|
192
198
|
|
|
@@ -374,6 +380,11 @@ class ClaudeManagedSession implements ProviderSession {
|
|
|
374
380
|
* 2. Anthropic's $0.08/session-hour runtime fee — billed continuously by
|
|
375
381
|
* Anthropic regardless of model usage, so we add it here to surface in
|
|
376
382
|
* the swarm's per-session cost UI.
|
|
383
|
+
*
|
|
384
|
+
* Phase 5: the harness-local USD is still computed here, but the server-side
|
|
385
|
+
* recompute path (`POST /api/session-costs` after Phase 2) will reprice the
|
|
386
|
+
* row against the seeded pricing-table values and tag `costSource='pricing-table'`.
|
|
387
|
+
* The runtime fee comes from the same table now (`token_class='runtime_hour'`).
|
|
377
388
|
*/
|
|
378
389
|
private snapshotCost(isError: boolean): CostData {
|
|
379
390
|
const durationMs = Date.now() - this.startedAt;
|
|
@@ -384,9 +395,11 @@ class ClaudeManagedSession implements ProviderSession {
|
|
|
384
395
|
this.cost.cacheReadTokens ?? 0,
|
|
385
396
|
this.cost.cacheWriteTokens ?? 0,
|
|
386
397
|
);
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
|
|
398
|
+
// Phase 5: read the runtime fee from the pricing table when available so
|
|
399
|
+
// we have one source of truth. Falls back to the historical $0.08/hr
|
|
400
|
+
// constant if the row hasn't been seeded yet (e.g. on a fresh DB before
|
|
401
|
+
// seed-pricing.ts ran).
|
|
402
|
+
const runtimeFeeUsd = (durationMs / 3_600_000) * getRuntimeFeePerHour();
|
|
390
403
|
return {
|
|
391
404
|
...this.cost,
|
|
392
405
|
durationMs,
|
|
@@ -506,12 +519,15 @@ class ClaudeManagedSession implements ProviderSession {
|
|
|
506
519
|
// this event. Emit a `compaction` ProviderEvent with the values we
|
|
507
520
|
// *do* know; consumers that need richer data can subscribe to
|
|
508
521
|
// `raw_log` for the original payload.
|
|
522
|
+
// Phase 5 — pre-compact tokens are an inferred proxy (running input
|
|
523
|
+
// total); flag the compactTrigger as 'auto-inferred' so downstream
|
|
524
|
+
// dashboards can distinguish a real trigger value from our guess.
|
|
509
525
|
const _cc = event as BetaManagedAgentsAgentThreadContextCompactedEvent;
|
|
510
526
|
this.emit({
|
|
511
527
|
type: "compaction",
|
|
512
528
|
preCompactTokens: this.cost.inputTokens ?? 0,
|
|
513
|
-
compactTrigger: "auto",
|
|
514
|
-
contextTotalTokens:
|
|
529
|
+
compactTrigger: "auto-inferred",
|
|
530
|
+
contextTotalTokens: getContextWindowSize(this.cost.model),
|
|
515
531
|
});
|
|
516
532
|
return { terminal: false, isError: false };
|
|
517
533
|
}
|
|
@@ -524,16 +540,26 @@ class ClaudeManagedSession implements ProviderSession {
|
|
|
524
540
|
(this.cost.cacheReadTokens ?? 0) + usage.cache_read_input_tokens;
|
|
525
541
|
this.cost.cacheWriteTokens =
|
|
526
542
|
(this.cost.cacheWriteTokens ?? 0) + usage.cache_creation_input_tokens;
|
|
527
|
-
this.cost.numTurns
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
543
|
+
this.cost.numTurns = (this.cost.numTurns ?? 0) + 1;
|
|
544
|
+
|
|
545
|
+
// Phase 5 + Phase 9: unified `input + cache + output` formula AND a
|
|
546
|
+
// per-model window via `getContextWindowSize`. Previously this used
|
|
547
|
+
// a hardcoded 1M window and ignored cache — fine for sonnet/opus,
|
|
548
|
+
// wrong for haiku and any future smaller-window model.
|
|
549
|
+
const used = computeContextUsedUnified({
|
|
550
|
+
inputTokens: this.cost.inputTokens,
|
|
551
|
+
cacheReadTokens: this.cost.cacheReadTokens,
|
|
552
|
+
cacheCreateTokens: this.cost.cacheWriteTokens,
|
|
553
|
+
outputTokens: this.cost.outputTokens,
|
|
554
|
+
});
|
|
555
|
+
const total = getContextWindowSize(this.cost.model);
|
|
531
556
|
this.emit({
|
|
532
557
|
type: "context_usage",
|
|
533
558
|
contextUsedTokens: used,
|
|
534
559
|
contextTotalTokens: total,
|
|
535
|
-
contextPercent:
|
|
560
|
+
contextPercent: clampContextPercent(used, total),
|
|
536
561
|
outputTokens: this.cost.outputTokens ?? 0,
|
|
562
|
+
contextFormula: CONTEXT_FORMULA,
|
|
537
563
|
});
|
|
538
564
|
return { terminal: false, isError: false };
|
|
539
565
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 5 — small adapter-side pricing constants for claude-managed.
|
|
3
|
+
*
|
|
4
|
+
* The API server's pricing table is the canonical store (seeded by
|
|
5
|
+
* `src/be/seed-pricing.ts`). Workers can't touch the DB directly (DB
|
|
6
|
+
* boundary), so the adapter keeps a local constant for the runtime fee
|
|
7
|
+
* and lets the API-side recompute path (Phase 2) override the resulting
|
|
8
|
+
* `totalCostUsd` with the canonical figure. The constant here is what
|
|
9
|
+
* shows up in the worker's local logs before the row hits the server.
|
|
10
|
+
*
|
|
11
|
+
* If/when we plumb pricing through the worker bootstrap (HTTP fetch of
|
|
12
|
+
* `/api/pricing` at session start), this module is the place to swap.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* USD per session-hour for managed claude runtime. Source:
|
|
17
|
+
* https://docs.claude.com/en/api/agent-sdk/managed-runtime#pricing
|
|
18
|
+
* (verified 2026-04-28). Override at runtime via env for ops bumps without
|
|
19
|
+
* a redeploy.
|
|
20
|
+
*/
|
|
21
|
+
export const RUNTIME_FEE_USD_PER_HOUR = (() => {
|
|
22
|
+
const raw = process.env.CLAUDE_MANAGED_RUNTIME_FEE_USD_PER_HOUR;
|
|
23
|
+
const n = raw ? Number(raw) : NaN;
|
|
24
|
+
if (Number.isFinite(n) && n >= 0) return n;
|
|
25
|
+
return 0.08;
|
|
26
|
+
})();
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Adapter helper. Always returns a finite number — never crashes the
|
|
30
|
+
* cost snapshot.
|
|
31
|
+
*/
|
|
32
|
+
export function getRuntimeFeePerHour(): number {
|
|
33
|
+
return RUNTIME_FEE_USD_PER_HOUR;
|
|
34
|
+
}
|