@senad-d/observme 0.1.3 → 0.1.5
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/.env.example +4 -0
- package/CHANGELOG.md +48 -0
- package/README.md +36 -11
- package/dashboards/observme-agent-node-graphs.json +5 -5
- package/dashboards/observme-agents.json +169 -4
- package/dashboards/observme-alerts.yaml +16 -3
- package/dashboards/observme-overview.json +6 -6
- package/dashboards/observme-trace-journey.json +4 -4
- package/docs/agent-subagent-observability-requirements.md +19 -10
- package/docs/compatibility-matrix.md +21 -4
- package/docs/configuration.md +7 -2
- package/docs/extension-integration.md +20 -13
- package/docs/reference/03-pi-event-and-session-model.md +6 -6
- package/docs/reference/04-telemetry-semantic-conventions.md +39 -1
- package/docs/reference/05-otel-pipeline-and-collector.md +23 -10
- package/docs/reference/06-security-privacy-redaction.md +13 -6
- package/docs/reference/08-query-grafana-integration.md +30 -7
- package/docs/reference/09-dashboards-alerts-slos.md +132 -3
- package/docs/reference/10-testing-release-operations.md +44 -14
- package/docs/reference/11-deployment-runbooks.md +82 -5
- package/docs/reference/12-configuration-reference.md +48 -5
- package/docs/review-validation.md +17 -0
- package/examples/README.md +7 -2
- package/examples/collector.yaml +8 -8
- package/examples/integrations/subagent-runner.ts +8 -5
- package/examples/observme.yaml +3 -2
- package/package.json +14 -4
- package/src/commands/obs-agents.ts +17 -12
- package/src/commands/obs-backfill.ts +2 -2
- package/src/commands/obs-command-support.ts +2 -1
- package/src/commands/obs-cost.ts +15 -7
- package/src/commands/obs-health.ts +22 -2
- package/src/commands/obs-loki-summary.ts +4 -8
- package/src/commands/obs-session.ts +35 -28
- package/src/commands/obs-status.ts +56 -13
- package/src/commands/obs-tools.ts +19 -8
- package/src/commands/obs-trace.ts +9 -0
- package/src/commands/obs.ts +6 -1
- package/src/config/bootstrap-project-config.ts +50 -32
- package/src/config/defaults.ts +1 -2
- package/src/config/load-config.ts +270 -81
- package/src/config/project-paths.ts +318 -6
- package/src/config/query-limits.ts +10 -0
- package/src/config/schema.ts +18 -8
- package/src/config/transport-security.ts +107 -0
- package/src/config/validate.ts +88 -12
- package/src/extension.ts +2 -12
- package/src/integration.ts +6 -2
- package/src/otel/logs.ts +24 -13
- package/src/otel/metrics.ts +24 -13
- package/src/otel/otlp-endpoint.ts +41 -2
- package/src/otel/otlp-http-options.ts +11 -0
- package/src/otel/sdk.ts +155 -9
- package/src/otel/shutdown.ts +39 -11
- package/src/otel/traces.ts +34 -16
- package/src/pi/active-agent-lease.ts +101 -0
- package/src/pi/agent-tree-tracker.ts +14 -1
- package/src/pi/compatibility.ts +121 -0
- package/src/pi/event-handlers/agent-turn.ts +16 -9
- package/src/pi/event-handlers/lifecycle.ts +312 -106
- package/src/pi/event-handlers/llm.ts +17 -9
- package/src/pi/event-handlers/session-events.ts +53 -19
- package/src/pi/event-handlers/tool-bash.ts +21 -27
- package/src/pi/handler-internals.ts +16 -26
- package/src/pi/handler-runtime.ts +159 -54
- package/src/pi/handler-types.ts +40 -17
- package/src/pi/handlers.ts +12 -1
- package/src/pi/integration-api.ts +27 -15
- package/src/pi/session-correlation.ts +167 -0
- package/src/pi/subagent-spawn.ts +164 -31
- package/src/privacy/redact.ts +574 -68
- package/src/privacy/secret-patterns.ts +6 -1
- package/src/query/grafana-readiness.ts +18 -2
- package/src/query/grafana-transport.ts +150 -27
- package/src/query/grafana-url.ts +36 -0
- package/src/query/grafana.ts +4 -136
- package/src/query/loki.ts +2 -4
- package/src/query/prometheus.ts +8 -10
- package/src/query/tempo.ts +2 -4
- package/src/query/trace-link.ts +186 -0
- package/src/safety/display-bounds.ts +84 -0
- package/src/semconv/metrics.ts +7 -0
- package/src/semconv/values.ts +2 -0
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
endAgentJoin,
|
|
20
20
|
endAgentWait,
|
|
21
21
|
failSubagentSpawn,
|
|
22
|
+
resolveSubagentSpawnIdentity,
|
|
22
23
|
startAgentJoin,
|
|
23
24
|
startAgentWait,
|
|
24
25
|
startSubagentSpawn,
|
|
@@ -94,11 +95,15 @@ export class SessionBackedObservMeIntegrationApi implements ObservMeIntegrationA
|
|
|
94
95
|
if (!session) return integrationFailure("session_unavailable");
|
|
95
96
|
try {
|
|
96
97
|
if (!isValidStartSubagentOptions(options)) return integrationFailure("invalid_request");
|
|
97
|
-
|
|
98
|
+
const identity = resolveSubagentSpawnIdentity(options);
|
|
99
|
+
if (session.spans.activeSubagentSpawns.has(identity.spawnId)) {
|
|
98
100
|
return integrationFailure("spawn_already_exists");
|
|
99
101
|
}
|
|
102
|
+
if (isChildAgentIdentifierRetained(session, identity.childAgentId)) {
|
|
103
|
+
return integrationFailure("child_agent_already_exists");
|
|
104
|
+
}
|
|
100
105
|
|
|
101
|
-
const started = startSubagentSpawn(session, options);
|
|
106
|
+
const started = startSubagentSpawn(session, { ...options, ...identity });
|
|
102
107
|
return {
|
|
103
108
|
ok: true,
|
|
104
109
|
spawnId: started.spawnId,
|
|
@@ -121,10 +126,8 @@ export class SessionBackedObservMeIntegrationApi implements ObservMeIntegrationA
|
|
|
121
126
|
if (!isValidIntegrationIdentifier(spawnId) || !isValidCompleteSubagentOptions(options)) {
|
|
122
127
|
return integrationFailure("invalid_request");
|
|
123
128
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
completeSubagentSpawn(session, spawnId, options);
|
|
127
|
-
return integrationSuccess();
|
|
129
|
+
const result = completeSubagentSpawn(session, spawnId, options);
|
|
130
|
+
return result.ok ? integrationSuccess() : integrationFailure(result.reason);
|
|
128
131
|
} catch {
|
|
129
132
|
return integrationFailure("operation_failed");
|
|
130
133
|
}
|
|
@@ -140,10 +143,8 @@ export class SessionBackedObservMeIntegrationApi implements ObservMeIntegrationA
|
|
|
140
143
|
if (!isValidIntegrationIdentifier(spawnId) || !isValidFailSubagentOptions(options)) {
|
|
141
144
|
return integrationFailure("invalid_request");
|
|
142
145
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
failSubagentSpawn(session, spawnId, options);
|
|
146
|
-
return integrationSuccess();
|
|
146
|
+
const result = failSubagentSpawn(session, spawnId, options);
|
|
147
|
+
return result.ok ? integrationSuccess() : integrationFailure(result.reason);
|
|
147
148
|
} catch {
|
|
148
149
|
return integrationFailure("operation_failed");
|
|
149
150
|
}
|
|
@@ -207,15 +208,22 @@ export class SessionBackedObservMeIntegrationApi implements ObservMeIntegrationA
|
|
|
207
208
|
const registry = kind === "wait" ? session.spans.activeAgentWaits : session.spans.activeAgentJoins;
|
|
208
209
|
if (!registry.has(id)) return integrationFailure(kind === "wait" ? "wait_not_found" : "join_not_found");
|
|
209
210
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return integrationSuccess();
|
|
211
|
+
const result = kind === "wait" ? endAgentWait(session, id, options) : endAgentJoin(session, id, options);
|
|
212
|
+
return result.ok ? integrationSuccess() : integrationFailure(result.reason);
|
|
213
213
|
} catch {
|
|
214
214
|
return integrationFailure("operation_failed");
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
function isChildAgentIdentifierRetained(session: ObservMeTelemetrySession, childAgentId: string): boolean {
|
|
220
|
+
if (session.agentTree.getAgent(childAgentId)) return true;
|
|
221
|
+
for (const activeSpawn of session.spans.activeSubagentSpawns.values()) {
|
|
222
|
+
if (activeSpawn.childAgentId === childAgentId) return true;
|
|
223
|
+
}
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
219
227
|
function resolveIntegrationEventBus(pi: unknown): IntegrationEventBus | undefined {
|
|
220
228
|
if (!pi || typeof pi !== "object") return undefined;
|
|
221
229
|
try {
|
|
@@ -256,8 +264,8 @@ function isValidCompleteSubagentOptions(value: unknown): value is ObservMeComple
|
|
|
256
264
|
const options = value as Partial<ObservMeCompleteSubagentOptions>;
|
|
257
265
|
return (
|
|
258
266
|
isOptionalIntegrationIdentifier(options.childAgentId) &&
|
|
259
|
-
|
|
260
|
-
(options.outcome
|
|
267
|
+
isOptionalTerminalChildStatus(options.childStatus) &&
|
|
268
|
+
isOptionalTerminalChildStatus(options.outcome)
|
|
261
269
|
);
|
|
262
270
|
}
|
|
263
271
|
|
|
@@ -349,6 +357,10 @@ function isOptionalChildStatus(value: unknown): boolean {
|
|
|
349
357
|
);
|
|
350
358
|
}
|
|
351
359
|
|
|
360
|
+
function isOptionalTerminalChildStatus(value: unknown): boolean {
|
|
361
|
+
return value === undefined || value === "completed" || value === "failed" || value === "cancelled";
|
|
362
|
+
}
|
|
363
|
+
|
|
352
364
|
function isOptionalJoinStatus(value: unknown): boolean {
|
|
353
365
|
return (
|
|
354
366
|
value === undefined ||
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { OBSERVME_CORRELATION_ENTRY_TYPE } from "../semconv/values.ts";
|
|
3
|
+
import type { AgentLineageContext } from "./agent-lineage.ts";
|
|
4
|
+
import type { MinimalSessionCorrelation } from "./handler-types.ts";
|
|
5
|
+
|
|
6
|
+
export { OBSERVME_CORRELATION_ENTRY_TYPE } from "../semconv/values.ts";
|
|
7
|
+
export const OBSERVME_CORRELATION_ENTRY_SCHEMA_VERSION = 1;
|
|
8
|
+
|
|
9
|
+
const maximumCorrelationValueLength = 128;
|
|
10
|
+
const correlationValuePattern = /^[A-Za-z0-9._:-]+$/u;
|
|
11
|
+
const correlationDataKeys = new Set([
|
|
12
|
+
"schemaVersion",
|
|
13
|
+
"workflowId",
|
|
14
|
+
"agentId",
|
|
15
|
+
"parentAgentId",
|
|
16
|
+
"rootAgentId",
|
|
17
|
+
"parentSessionId",
|
|
18
|
+
"depth",
|
|
19
|
+
"spawnId",
|
|
20
|
+
"capability",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
export interface ObservMeCorrelationEntryData extends MinimalSessionCorrelation {
|
|
24
|
+
readonly schemaVersion: typeof OBSERVME_CORRELATION_ENTRY_SCHEMA_VERSION;
|
|
25
|
+
readonly workflowId: string;
|
|
26
|
+
readonly agentId: string;
|
|
27
|
+
readonly rootAgentId: string;
|
|
28
|
+
readonly depth: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function readLatestSessionCorrelation(entries: readonly unknown[] | undefined): MinimalSessionCorrelation | undefined {
|
|
32
|
+
if (!entries) return undefined;
|
|
33
|
+
|
|
34
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
35
|
+
const correlation = readSessionCorrelationEntry(entries[index]);
|
|
36
|
+
if (correlation) return correlation;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function appendSessionCorrelationEntry(
|
|
43
|
+
appendEntry: ExtensionAPI["appendEntry"] | undefined,
|
|
44
|
+
lineage: AgentLineageContext,
|
|
45
|
+
recovered: MinimalSessionCorrelation | undefined,
|
|
46
|
+
): void {
|
|
47
|
+
if (!appendEntry) return;
|
|
48
|
+
|
|
49
|
+
const data = buildSessionCorrelationEntryData(lineage);
|
|
50
|
+
if (!data || correlationsEqual(data, recovered)) return;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
appendEntry(OBSERVME_CORRELATION_ENTRY_TYPE, data);
|
|
54
|
+
} catch {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function buildSessionCorrelationEntryData(
|
|
60
|
+
lineage: AgentLineageContext,
|
|
61
|
+
): ObservMeCorrelationEntryData | undefined {
|
|
62
|
+
return normalizeSessionCorrelationData({
|
|
63
|
+
schemaVersion: OBSERVME_CORRELATION_ENTRY_SCHEMA_VERSION,
|
|
64
|
+
workflowId: lineage.workflowId,
|
|
65
|
+
agentId: lineage.agentId,
|
|
66
|
+
parentAgentId: lineage.parentAgentId,
|
|
67
|
+
rootAgentId: lineage.rootAgentId,
|
|
68
|
+
parentSessionId: lineage.parentSessionId,
|
|
69
|
+
depth: lineage.depth,
|
|
70
|
+
spawnId: lineage.spawnId,
|
|
71
|
+
capability: lineage.capability,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readSessionCorrelationEntry(value: unknown): ObservMeCorrelationEntryData | undefined {
|
|
76
|
+
if (!isRecord(value)) return undefined;
|
|
77
|
+
if (value.type !== "custom" || value.customType !== OBSERVME_CORRELATION_ENTRY_TYPE) return undefined;
|
|
78
|
+
return normalizeSessionCorrelationData(value.data);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeSessionCorrelationData(value: unknown): ObservMeCorrelationEntryData | undefined {
|
|
82
|
+
if (!isRecord(value) || !hasOnlyCorrelationDataKeys(value)) return undefined;
|
|
83
|
+
if (value.schemaVersion !== OBSERVME_CORRELATION_ENTRY_SCHEMA_VERSION) return undefined;
|
|
84
|
+
|
|
85
|
+
const workflowId = readCorrelationString(value, "workflowId");
|
|
86
|
+
const agentId = readCorrelationString(value, "agentId");
|
|
87
|
+
const parentAgentId = readOptionalCorrelationString(value, "parentAgentId");
|
|
88
|
+
const rootAgentId = readCorrelationString(value, "rootAgentId");
|
|
89
|
+
const parentSessionId = readOptionalCorrelationString(value, "parentSessionId");
|
|
90
|
+
const depth = readCorrelationDepth(value.depth);
|
|
91
|
+
const spawnId = readOptionalCorrelationString(value, "spawnId");
|
|
92
|
+
const capability = readOptionalCorrelationString(value, "capability");
|
|
93
|
+
|
|
94
|
+
if (!workflowId || !agentId || !rootAgentId || depth === undefined) return undefined;
|
|
95
|
+
if (parentAgentId === null || parentSessionId === null || spawnId === null || capability === null) return undefined;
|
|
96
|
+
if (!isConsistentCorrelationLineage(agentId, parentAgentId, rootAgentId, depth)) return undefined;
|
|
97
|
+
|
|
98
|
+
return withoutUndefinedValues({
|
|
99
|
+
schemaVersion: OBSERVME_CORRELATION_ENTRY_SCHEMA_VERSION,
|
|
100
|
+
workflowId,
|
|
101
|
+
agentId,
|
|
102
|
+
parentAgentId,
|
|
103
|
+
rootAgentId,
|
|
104
|
+
parentSessionId,
|
|
105
|
+
depth,
|
|
106
|
+
spawnId,
|
|
107
|
+
capability,
|
|
108
|
+
}) as ObservMeCorrelationEntryData;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function hasOnlyCorrelationDataKeys(value: Record<string, unknown>): boolean {
|
|
112
|
+
return Object.keys(value).every(key => correlationDataKeys.has(key));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function readCorrelationString(value: Record<string, unknown>, key: string): string | undefined {
|
|
116
|
+
const candidate = value[key];
|
|
117
|
+
if (typeof candidate !== "string") return undefined;
|
|
118
|
+
if (candidate.length === 0 || candidate.length > maximumCorrelationValueLength) return undefined;
|
|
119
|
+
return correlationValuePattern.test(candidate) ? candidate : undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function readOptionalCorrelationString(
|
|
123
|
+
value: Record<string, unknown>,
|
|
124
|
+
key: string,
|
|
125
|
+
): string | undefined | null {
|
|
126
|
+
if (!Object.hasOwn(value, key) || value[key] === undefined) return undefined;
|
|
127
|
+
return readCorrelationString(value, key) ?? null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function readCorrelationDepth(value: unknown): number | undefined {
|
|
131
|
+
if (typeof value !== "number" || !Number.isInteger(value)) return undefined;
|
|
132
|
+
return value >= 0 && value <= 64 ? value : undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isConsistentCorrelationLineage(
|
|
136
|
+
agentId: string,
|
|
137
|
+
parentAgentId: string | undefined,
|
|
138
|
+
rootAgentId: string,
|
|
139
|
+
depth: number,
|
|
140
|
+
): boolean {
|
|
141
|
+
if (depth === 0) return parentAgentId === undefined && rootAgentId === agentId;
|
|
142
|
+
return parentAgentId !== undefined && parentAgentId !== agentId;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function correlationsEqual(
|
|
146
|
+
left: ObservMeCorrelationEntryData,
|
|
147
|
+
right: MinimalSessionCorrelation | undefined,
|
|
148
|
+
): boolean {
|
|
149
|
+
if (!right) return false;
|
|
150
|
+
|
|
151
|
+
return left.workflowId === right.workflowId
|
|
152
|
+
&& left.agentId === right.agentId
|
|
153
|
+
&& left.parentAgentId === right.parentAgentId
|
|
154
|
+
&& left.rootAgentId === right.rootAgentId
|
|
155
|
+
&& left.parentSessionId === right.parentSessionId
|
|
156
|
+
&& left.depth === right.depth
|
|
157
|
+
&& left.spawnId === right.spawnId
|
|
158
|
+
&& left.capability === right.capability;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function withoutUndefinedValues<T extends Record<string, unknown>>(value: T): T {
|
|
162
|
+
return Object.fromEntries(Object.entries(value).filter(entry => entry[1] !== undefined)) as T;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
166
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
167
|
+
}
|
package/src/pi/subagent-spawn.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
} from "../semconv/values.ts";
|
|
25
25
|
import { BoundedMap } from "../util/bounded-map.ts";
|
|
26
26
|
import type { AgentChildStatus, AgentTreeNode, AgentTreeSummary } from "./agent-tree-tracker.ts";
|
|
27
|
-
import { AgentTreeTracker } from "./agent-tree-tracker.ts";
|
|
27
|
+
import { AgentTreeTracker, isAgentStatusTransitionAllowed } from "./agent-tree-tracker.ts";
|
|
28
28
|
import type { AgentLineageContext, AgentRole } from "./agent-lineage.ts";
|
|
29
29
|
import { createAgentLineageContext, createPropagationEnvironment, sanitizePropagationEnvironment } from "./agent-lineage.ts";
|
|
30
30
|
import { recordActiveSpanEnd, recordActiveSpanStart } from "./handler-internals.ts";
|
|
@@ -44,6 +44,7 @@ export type {
|
|
|
44
44
|
|
|
45
45
|
export type SubagentSpawnType = "command" | "tool" | "extension" | "unknown";
|
|
46
46
|
export type AgentJoinStatus = "completed" | "failed" | "cancelled" | "timeout" | "unknown" | "waiting";
|
|
47
|
+
export type AgentTerminalStatus = Extract<AgentChildStatus, "completed" | "failed" | "cancelled">;
|
|
47
48
|
export type AttributePrimitive = boolean | number | string | string[];
|
|
48
49
|
export type AttributeMap = Record<string, AttributePrimitive>;
|
|
49
50
|
|
|
@@ -125,13 +126,35 @@ export interface StartedSubagentSpawn {
|
|
|
125
126
|
readonly attributes: AttributeMap;
|
|
126
127
|
}
|
|
127
128
|
|
|
129
|
+
export interface SubagentSpawnIdentity {
|
|
130
|
+
readonly spawnId: string;
|
|
131
|
+
readonly childAgentId: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
128
134
|
export interface CompleteSubagentSpawnOptions {
|
|
129
135
|
readonly childAgentId?: string;
|
|
130
|
-
readonly childStatus?:
|
|
131
|
-
readonly outcome?:
|
|
136
|
+
readonly childStatus?: AgentTerminalStatus;
|
|
137
|
+
readonly outcome?: AgentTerminalStatus;
|
|
132
138
|
readonly now?: () => number;
|
|
133
139
|
}
|
|
134
140
|
|
|
141
|
+
export type SubagentTransitionFailureReason =
|
|
142
|
+
| "spawn_not_found"
|
|
143
|
+
| "child_agent_mismatch"
|
|
144
|
+
| "invalid_terminal_transition";
|
|
145
|
+
|
|
146
|
+
export type SubagentTransitionResult =
|
|
147
|
+
| { readonly ok: true }
|
|
148
|
+
| { readonly ok: false; readonly reason: SubagentTransitionFailureReason };
|
|
149
|
+
|
|
150
|
+
interface ResolvedCompletionTransition {
|
|
151
|
+
readonly ok: true;
|
|
152
|
+
readonly childAgentId: string;
|
|
153
|
+
readonly status: AgentTerminalStatus;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
type CompletionTransitionResult = ResolvedCompletionTransition | Extract<SubagentTransitionResult, { readonly ok: false }>;
|
|
157
|
+
|
|
135
158
|
export interface FailSubagentSpawnOptions {
|
|
136
159
|
readonly childAgentId?: string;
|
|
137
160
|
readonly errorClass?: string;
|
|
@@ -219,12 +242,18 @@ export async function runSubagentWithObservability<Result>(
|
|
|
219
242
|
}
|
|
220
243
|
}
|
|
221
244
|
|
|
245
|
+
export function resolveSubagentSpawnIdentity(
|
|
246
|
+
options: Pick<StartSubagentSpawnOptions, "spawnId" | "childAgentId"> = {},
|
|
247
|
+
): SubagentSpawnIdentity {
|
|
248
|
+
const spawnId = options.spawnId ?? `spawn-${randomUUID()}`;
|
|
249
|
+
return { spawnId, childAgentId: options.childAgentId ?? `child-${spawnId}` };
|
|
250
|
+
}
|
|
251
|
+
|
|
222
252
|
export function startSubagentSpawn(
|
|
223
253
|
session: SubagentTelemetrySession,
|
|
224
254
|
options: StartSubagentSpawnOptions = {},
|
|
225
255
|
): StartedSubagentSpawn {
|
|
226
|
-
const spawnId
|
|
227
|
-
const childAgentId = options.childAgentId ?? `child-${spawnId}`;
|
|
256
|
+
const { spawnId, childAgentId } = resolveSubagentSpawnIdentity(options);
|
|
228
257
|
const spawnReason = normalizeSpawnReason(options.spawnReason);
|
|
229
258
|
const labels = subagentSpawnMetricLabels(session, options, spawnReason);
|
|
230
259
|
const parentSpan = resolveSubagentParentSpan(session);
|
|
@@ -269,48 +298,55 @@ export function completeSubagentSpawn(
|
|
|
269
298
|
session: SubagentTelemetrySession,
|
|
270
299
|
spawnId: string,
|
|
271
300
|
options: CompleteSubagentSpawnOptions = {},
|
|
272
|
-
):
|
|
301
|
+
): SubagentTransitionResult {
|
|
273
302
|
const state = session.spans.activeSubagentSpawns.get(spawnId);
|
|
274
|
-
if (!state) return;
|
|
303
|
+
if (!state) return subagentTransitionFailure("spawn_not_found");
|
|
275
304
|
|
|
276
|
-
const
|
|
277
|
-
|
|
305
|
+
const transition = resolveCompletionTransition(session, state, options);
|
|
306
|
+
if (!transition.ok) return transition;
|
|
307
|
+
|
|
308
|
+
updateChildStatus(session, transition.childAgentId, transition.status);
|
|
309
|
+
recordChildFailureCompletion(session, transition.childAgentId, transition.status);
|
|
310
|
+
recordObsAgentsTreeState(session);
|
|
278
311
|
const attributes = buildSubagentCompletionAttributes(
|
|
279
312
|
session,
|
|
280
313
|
spawnId,
|
|
281
|
-
childAgentId,
|
|
282
|
-
|
|
314
|
+
transition.childAgentId,
|
|
315
|
+
transition.status,
|
|
283
316
|
state.spawnReason,
|
|
284
317
|
);
|
|
318
|
+
const eventName = terminalSpawnEventName(transition.status);
|
|
319
|
+
const severityText = transition.status === "completed" ? "INFO" : "ERROR";
|
|
285
320
|
|
|
286
|
-
updateChildStatus(session, childAgentId, childStatus);
|
|
287
|
-
recordChildFailureCompletion(session, childAgentId, childStatus);
|
|
288
|
-
recordObsAgentsTreeState(session);
|
|
289
321
|
recordSubagentSpawnDuration(session, state, options);
|
|
290
322
|
state.span.setAttributes(attributes);
|
|
291
|
-
state.span
|
|
292
|
-
state.span.addEvent(
|
|
323
|
+
setTerminalSpawnStatus(state.span, transition.status);
|
|
324
|
+
state.span.addEvent(eventName, attributes);
|
|
293
325
|
endSubagentSpan(session, state.span);
|
|
294
326
|
session.spans.activeSubagentSpawns.delete(spawnId);
|
|
295
|
-
emitSubagentLog(session,
|
|
327
|
+
emitSubagentLog(session, eventName, attributes, severityText);
|
|
328
|
+
return subagentTransitionSuccess();
|
|
296
329
|
}
|
|
297
330
|
|
|
298
331
|
export function failSubagentSpawn(
|
|
299
332
|
session: SubagentTelemetrySession,
|
|
300
333
|
spawnId: string,
|
|
301
334
|
options: FailSubagentSpawnOptions = {},
|
|
302
|
-
):
|
|
335
|
+
): SubagentTransitionResult {
|
|
303
336
|
const state = session.spans.activeSubagentSpawns.get(spawnId);
|
|
304
|
-
if (!state) return;
|
|
337
|
+
if (!state) return subagentTransitionFailure("spawn_not_found");
|
|
305
338
|
|
|
306
339
|
const childAgentId = options.childAgentId ?? state.childAgentId;
|
|
340
|
+
const transitionFailure = validateLauncherFailureTransition(session, state, childAgentId);
|
|
341
|
+
if (transitionFailure) return transitionFailure;
|
|
342
|
+
|
|
343
|
+
updateChildStatus(session, childAgentId, "failed");
|
|
344
|
+
recordObsAgentsTreeState(session);
|
|
307
345
|
const attributes = {
|
|
308
346
|
...buildSubagentCompletionAttributes(session, spawnId, childAgentId, "failed", state.spawnReason),
|
|
309
347
|
[LOG_ATTRIBUTES.ERROR_TYPE]: normalizeMetricLabel(options.errorClass ?? "subagent_spawn_error", "subagent_spawn_error"),
|
|
310
348
|
};
|
|
311
349
|
|
|
312
|
-
updateChildStatus(session, childAgentId, "failed");
|
|
313
|
-
recordObsAgentsTreeState(session);
|
|
314
350
|
recordSubagentSpawnDuration(session, state, options);
|
|
315
351
|
state.span.setAttributes(attributes);
|
|
316
352
|
state.span.setStatus({ code: SpanStatusCode.ERROR, message: String(attributes[LOG_ATTRIBUTES.ERROR_TYPE]) });
|
|
@@ -319,6 +355,7 @@ export function failSubagentSpawn(
|
|
|
319
355
|
session.spans.activeSubagentSpawns.delete(spawnId);
|
|
320
356
|
session.metrics.subagentSpawnFailures.add(1, subagentFailureMetricLabels(state.labels, attributes));
|
|
321
357
|
emitSubagentLog(session, LOG_EVENT_NAMES.AGENT_SPAWN_FAILED, attributes, "ERROR");
|
|
358
|
+
return subagentTransitionSuccess();
|
|
322
359
|
}
|
|
323
360
|
|
|
324
361
|
export function buildSubagentPropagationEnvironment(
|
|
@@ -372,8 +409,8 @@ export function endAgentWait(
|
|
|
372
409
|
session: SubagentTelemetrySession,
|
|
373
410
|
waitId: string,
|
|
374
411
|
options: AgentWaitJoinOptions = {},
|
|
375
|
-
):
|
|
376
|
-
endWaitJoinSpan(session, waitId, options, "wait");
|
|
412
|
+
): SubagentTransitionResult {
|
|
413
|
+
return endWaitJoinSpan(session, waitId, options, "wait");
|
|
377
414
|
}
|
|
378
415
|
|
|
379
416
|
export function recordAgentWait(session: SubagentTelemetrySession, options: AgentWaitJoinOptions = {}): StartedAgentWaitJoin {
|
|
@@ -393,8 +430,8 @@ export function endAgentJoin(
|
|
|
393
430
|
session: SubagentTelemetrySession,
|
|
394
431
|
joinId: string,
|
|
395
432
|
options: AgentWaitJoinOptions = {},
|
|
396
|
-
):
|
|
397
|
-
endWaitJoinSpan(session, joinId, options, "join");
|
|
433
|
+
): SubagentTransitionResult {
|
|
434
|
+
return endWaitJoinSpan(session, joinId, options, "join");
|
|
398
435
|
}
|
|
399
436
|
|
|
400
437
|
export function recordAgentJoin(session: SubagentTelemetrySession, options: AgentWaitJoinOptions = {}): StartedAgentWaitJoin {
|
|
@@ -468,11 +505,13 @@ function endWaitJoinSpan(
|
|
|
468
505
|
id: string,
|
|
469
506
|
options: AgentWaitJoinOptions,
|
|
470
507
|
kind: "wait" | "join",
|
|
471
|
-
):
|
|
508
|
+
): SubagentTransitionResult {
|
|
472
509
|
const registry = waitJoinRegistry(session, kind);
|
|
473
510
|
const state = registry.get(id);
|
|
474
|
-
if (!state) return;
|
|
511
|
+
if (!state) return subagentTransitionFailure("spawn_not_found");
|
|
475
512
|
|
|
513
|
+
const transition = validateWaitJoinTransition(session, options, kind);
|
|
514
|
+
if (!transition.ok) return transition;
|
|
476
515
|
if (kind === "join" && options.childAgentId && options.childStatus) updateChildStatus(session, options.childAgentId, options.childStatus);
|
|
477
516
|
|
|
478
517
|
const attributes = buildWaitJoinAttributes(session, options, state.reason);
|
|
@@ -489,6 +528,28 @@ function endWaitJoinSpan(
|
|
|
489
528
|
recordWaitJoinDuration(session, durationMs, state.labels, kind);
|
|
490
529
|
if (kind === "join") recordChildJoinAccounting(session, options);
|
|
491
530
|
emitSubagentLog(session, eventName, attributes, waitJoinFailed(options) ? "ERROR" : "INFO");
|
|
531
|
+
return subagentTransitionSuccess();
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function validateWaitJoinTransition(
|
|
535
|
+
session: SubagentTelemetrySession,
|
|
536
|
+
options: AgentWaitJoinOptions,
|
|
537
|
+
kind: "wait" | "join",
|
|
538
|
+
): SubagentTransitionResult {
|
|
539
|
+
if (kind === "wait" || !options.childAgentId || !options.childStatus) return subagentTransitionSuccess();
|
|
540
|
+
|
|
541
|
+
if (options.spawnId) {
|
|
542
|
+
const spawn = session.spans.activeSubagentSpawns.get(options.spawnId);
|
|
543
|
+
if (spawn && spawn.childAgentId !== options.childAgentId) {
|
|
544
|
+
return subagentTransitionFailure("child_agent_mismatch");
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const child = ensureAgentTree(session).getAgent(options.childAgentId);
|
|
549
|
+
if (child && !isAgentStatusTransitionAllowed(child.status, options.childStatus)) {
|
|
550
|
+
return subagentTransitionFailure("invalid_terminal_transition");
|
|
551
|
+
}
|
|
552
|
+
return subagentTransitionSuccess();
|
|
492
553
|
}
|
|
493
554
|
|
|
494
555
|
function waitJoinRegistry(
|
|
@@ -523,7 +584,7 @@ function recordSubagentSpawnDuration(
|
|
|
523
584
|
function recordChildFailureCompletion(
|
|
524
585
|
session: SubagentTelemetrySession,
|
|
525
586
|
childAgentId: string,
|
|
526
|
-
childStatus:
|
|
587
|
+
childStatus: AgentTerminalStatus,
|
|
527
588
|
): void {
|
|
528
589
|
if (childStatus !== "failed") return;
|
|
529
590
|
recordChildFailureAccounting(session, childAgentId, false);
|
|
@@ -644,11 +705,83 @@ function buildSubagentSpawnAttributes(
|
|
|
644
705
|
});
|
|
645
706
|
}
|
|
646
707
|
|
|
708
|
+
function resolveCompletionTransition(
|
|
709
|
+
session: SubagentTelemetrySession,
|
|
710
|
+
state: SubagentSpawnState,
|
|
711
|
+
options: CompleteSubagentSpawnOptions,
|
|
712
|
+
): CompletionTransitionResult {
|
|
713
|
+
const childAgentId = options.childAgentId ?? state.childAgentId;
|
|
714
|
+
const requestedStatus: unknown = options.childStatus ?? options.outcome ?? "completed";
|
|
715
|
+
if (!isAgentTerminalStatus(requestedStatus)) return subagentTransitionFailure("invalid_terminal_transition");
|
|
716
|
+
if (options.childStatus !== undefined && options.outcome !== undefined && options.childStatus !== options.outcome) {
|
|
717
|
+
return subagentTransitionFailure("invalid_terminal_transition");
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const transitionFailure = validateSpawnChildTransition(session, state, childAgentId, requestedStatus);
|
|
721
|
+
if (transitionFailure) return transitionFailure;
|
|
722
|
+
return { ok: true, childAgentId, status: requestedStatus };
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function validateSpawnChildTransition(
|
|
726
|
+
session: SubagentTelemetrySession,
|
|
727
|
+
state: SubagentSpawnState,
|
|
728
|
+
childAgentId: string,
|
|
729
|
+
status: AgentTerminalStatus,
|
|
730
|
+
): Extract<SubagentTransitionResult, { readonly ok: false }> | undefined {
|
|
731
|
+
if (childAgentId !== state.childAgentId) return subagentTransitionFailure("child_agent_mismatch");
|
|
732
|
+
|
|
733
|
+
const child = ensureAgentTree(session).getAgent(childAgentId);
|
|
734
|
+
if (!child || !isAgentStatusTransitionAllowed(child.status, status)) {
|
|
735
|
+
return subagentTransitionFailure("invalid_terminal_transition");
|
|
736
|
+
}
|
|
737
|
+
return undefined;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function validateLauncherFailureTransition(
|
|
741
|
+
session: SubagentTelemetrySession,
|
|
742
|
+
state: SubagentSpawnState,
|
|
743
|
+
childAgentId: string,
|
|
744
|
+
): Extract<SubagentTransitionResult, { readonly ok: false }> | undefined {
|
|
745
|
+
if (childAgentId !== state.childAgentId) return subagentTransitionFailure("child_agent_mismatch");
|
|
746
|
+
|
|
747
|
+
const child = ensureAgentTree(session).getAgent(childAgentId);
|
|
748
|
+
if (child?.status !== "starting") return subagentTransitionFailure("invalid_terminal_transition");
|
|
749
|
+
return undefined;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function isAgentTerminalStatus(value: unknown): value is AgentTerminalStatus {
|
|
753
|
+
return value === "completed" || value === "failed" || value === "cancelled";
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function terminalSpawnEventName(status: AgentTerminalStatus): string {
|
|
757
|
+
if (status === "completed") return LOG_EVENT_NAMES.AGENT_SPAWN_COMPLETED;
|
|
758
|
+
if (status === "failed") return LOG_EVENT_NAMES.AGENT_SPAWN_FAILED;
|
|
759
|
+
return LOG_EVENT_NAMES.AGENT_SPAWN_CANCELLED;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function setTerminalSpawnStatus(span: Span, status: AgentTerminalStatus): void {
|
|
763
|
+
if (status === "completed") {
|
|
764
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: status });
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function subagentTransitionSuccess(): SubagentTransitionResult {
|
|
771
|
+
return { ok: true };
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function subagentTransitionFailure(
|
|
775
|
+
reason: SubagentTransitionFailureReason,
|
|
776
|
+
): Extract<SubagentTransitionResult, { readonly ok: false }> {
|
|
777
|
+
return { ok: false, reason };
|
|
778
|
+
}
|
|
779
|
+
|
|
647
780
|
function buildSubagentCompletionAttributes(
|
|
648
781
|
session: SubagentTelemetrySession,
|
|
649
782
|
spawnId: string,
|
|
650
783
|
childAgentId: string,
|
|
651
|
-
outcome:
|
|
784
|
+
outcome: AgentTerminalStatus,
|
|
652
785
|
spawnReason: SubagentSpawnReason,
|
|
653
786
|
): AttributeMap {
|
|
654
787
|
const summary = ensureAgentTree(session).summarize(session.lineage.rootAgentId);
|
|
@@ -736,8 +869,8 @@ function updateChildStatus(
|
|
|
736
869
|
session: SubagentTelemetrySession,
|
|
737
870
|
childAgentId: string,
|
|
738
871
|
status: AgentChildStatus,
|
|
739
|
-
):
|
|
740
|
-
ensureAgentTree(session).updateStatus(childAgentId, status);
|
|
872
|
+
): AgentTreeNode | undefined {
|
|
873
|
+
return ensureAgentTree(session).updateStatus(childAgentId, status);
|
|
741
874
|
}
|
|
742
875
|
|
|
743
876
|
function recordOrphanAgent(session: SubagentTelemetrySession, node: AgentTreeNode): void {
|