@vauban-org/agent-sdk 1.2.0 → 1.3.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/CONTRACT.md +595 -7
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/orchestration/ooda/agent.d.ts.map +1 -1
- package/dist/orchestration/ooda/agent.js +36 -0
- package/dist/orchestration/ooda/agent.js.map +1 -1
- package/dist/orchestration/ooda/types.d.ts +11 -0
- package/dist/orchestration/ooda/types.d.ts.map +1 -1
- package/dist/skills/_secrets.d.ts +16 -0
- package/dist/skills/_secrets.d.ts.map +1 -0
- package/dist/skills/_secrets.js +20 -0
- package/dist/skills/_secrets.js.map +1 -0
- package/dist/skills/alpaca-quote.d.ts +2 -2
- package/dist/skills/alpaca-quote.d.ts.map +1 -1
- package/dist/skills/alpaca-quote.js +51 -20
- package/dist/skills/alpaca-quote.js.map +1 -1
- package/dist/skills/send-email.d.ts +2 -2
- package/dist/skills/send-email.d.ts.map +1 -1
- package/dist/skills/send-email.js +1 -12
- package/dist/skills/send-email.js.map +1 -1
- package/dist/skills/slack-notify.d.ts.map +1 -1
- package/dist/skills/slack-notify.js +52 -21
- package/dist/skills/slack-notify.js.map +1 -1
- package/dist/skills/telegram-notify.d.ts.map +1 -1
- package/dist/skills/telegram-notify.js +48 -19
- package/dist/skills/telegram-notify.js.map +1 -1
- package/dist/skills/web-search.d.ts.map +1 -1
- package/dist/skills/web-search.js +85 -40
- package/dist/skills/web-search.js.map +1 -1
- package/dist/telemetry/bus.d.ts +54 -0
- package/dist/telemetry/bus.d.ts.map +1 -0
- package/dist/telemetry/bus.js +159 -0
- package/dist/telemetry/bus.js.map +1 -0
- package/dist/telemetry/index.d.ts +35 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +30 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/telemetry/port.d.ts +121 -0
- package/dist/telemetry/port.d.ts.map +1 -0
- package/dist/telemetry/port.js +48 -0
- package/dist/telemetry/port.js.map +1 -0
- package/dist/telemetry/sinks/otlp.d.ts +45 -0
- package/dist/telemetry/sinks/otlp.d.ts.map +1 -0
- package/dist/telemetry/sinks/otlp.js +195 -0
- package/dist/telemetry/sinks/otlp.js.map +1 -0
- package/dist/telemetry/sinks/sqlite.d.ts +32 -0
- package/dist/telemetry/sinks/sqlite.d.ts.map +1 -0
- package/dist/telemetry/sinks/sqlite.js +170 -0
- package/dist/telemetry/sinks/sqlite.js.map +1 -0
- package/dist/telemetry/sinks/stdout.d.ts +22 -0
- package/dist/telemetry/sinks/stdout.d.ts.map +1 -0
- package/dist/telemetry/sinks/stdout.js +38 -0
- package/dist/telemetry/sinks/stdout.js.map +1 -0
- package/docs/telemetry/migration.md +155 -0
- package/docs/telemetry/overview.md +154 -0
- package/docs/telemetry/privacy.md +127 -0
- package/docs/telemetry/sinks/cc.md +155 -0
- package/docs/telemetry/sinks/otlp.md +146 -0
- package/docs/telemetry/sinks/sqlite.md +126 -0
- package/docs/telemetry/sinks/stdout.md +82 -0
- package/package.json +18 -19
- package/src/index.ts +30 -1
- package/src/orchestration/ooda/agent.ts +50 -0
- package/src/orchestration/ooda/types.ts +12 -0
- package/src/skills/_secrets.ts +25 -0
- package/src/skills/alpaca-quote.ts +68 -23
- package/src/skills/send-email.ts +1 -12
- package/src/skills/slack-notify.ts +73 -30
- package/src/skills/telegram-notify.ts +70 -24
- package/src/skills/web-search.ts +132 -50
- package/src/telemetry/bus.test.ts +231 -0
- package/src/telemetry/bus.ts +241 -0
- package/src/telemetry/index.ts +49 -0
- package/src/telemetry/port.ts +160 -0
- package/src/telemetry/sinks/otlp.test.ts +146 -0
- package/src/telemetry/sinks/otlp.ts +250 -0
- package/src/telemetry/sinks/sqlite.test.ts +121 -0
- package/src/telemetry/sinks/sqlite.ts +260 -0
- package/src/telemetry/sinks/stdout.test.ts +109 -0
- package/src/telemetry/sinks/stdout.ts +59 -0
package/src/index.ts
CHANGED
|
@@ -105,10 +105,39 @@ export { createAgentRunTracker } from "./tracking/agent-run-tracker.js";
|
|
|
105
105
|
export type {
|
|
106
106
|
AgentRunTracker,
|
|
107
107
|
AgentRunStartInput,
|
|
108
|
-
AgentRunStepDelta,
|
|
109
108
|
AgentRunFinish,
|
|
110
109
|
DbClient,
|
|
111
110
|
} from "./tracking/agent-run-tracker.js";
|
|
111
|
+
|
|
112
|
+
// ─── Telemetry — sovereign multi-sink observability (ADR-ECO-039) ────────────
|
|
113
|
+
export {
|
|
114
|
+
NOOP_TELEMETRY_SINK,
|
|
115
|
+
TelemetrySinkError,
|
|
116
|
+
createTelemetryBus,
|
|
117
|
+
stdoutTelemetrySink,
|
|
118
|
+
localSqliteTelemetrySink,
|
|
119
|
+
otlpTelemetrySink,
|
|
120
|
+
} from "./telemetry/index.js";
|
|
121
|
+
export type {
|
|
122
|
+
TelemetrySink,
|
|
123
|
+
TelemetryRunStart,
|
|
124
|
+
TelemetryRunStep,
|
|
125
|
+
TelemetryRunFinish,
|
|
126
|
+
TelemetryRunStatus,
|
|
127
|
+
TelemetryBusOptions,
|
|
128
|
+
TelemetryCounters,
|
|
129
|
+
TelemetryLogger,
|
|
130
|
+
StdoutTelemetrySinkOptions,
|
|
131
|
+
LocalSqliteTelemetrySinkOptions,
|
|
132
|
+
OtlpTelemetrySinkOptions,
|
|
133
|
+
} from "./telemetry/index.js";
|
|
134
|
+
|
|
135
|
+
// Re-export legacy AgentRunStepDelta + AgentRunFinalStatus (kept for backward
|
|
136
|
+
// compat with `AgentRunTracker` consumers, e.g. command-center/src/tracking).
|
|
137
|
+
export type {
|
|
138
|
+
AgentRunStepDelta,
|
|
139
|
+
AgentRunFinalStatus,
|
|
140
|
+
} from "./tracking/agent-run-tracker.js";
|
|
112
141
|
export { createBullMQRunner, BullMQRunner } from "./durable/bullmq-runner.js";
|
|
113
142
|
export type {
|
|
114
143
|
BullMQRunnerConfig,
|
|
@@ -295,6 +295,51 @@ export class OODAAgentImpl<
|
|
|
295
295
|
const executionMode = overrideMode ?? this._config.executionMode;
|
|
296
296
|
const isReplay = false;
|
|
297
297
|
const logger = this._config.logger ?? NOOP_LOGGER;
|
|
298
|
+
const startedAt = new Date().toISOString();
|
|
299
|
+
|
|
300
|
+
// ── Telemetry (ADR-ECO-039) ─────────────────────────────────────────────
|
|
301
|
+
// Failures isolated by sink/bus — never block agent. Fire-and-forget on
|
|
302
|
+
// exit paths so cycle latency is unaffected.
|
|
303
|
+
const telemetry = this._config.telemetry;
|
|
304
|
+
const telemetryStartFired = telemetry
|
|
305
|
+
? telemetry
|
|
306
|
+
.start({
|
|
307
|
+
runId,
|
|
308
|
+
agentId: this._config.agentId,
|
|
309
|
+
agentVersion: this._config.agentVersion ?? "0.0.0",
|
|
310
|
+
model: "unknown",
|
|
311
|
+
provider: "unknown",
|
|
312
|
+
startedAt,
|
|
313
|
+
})
|
|
314
|
+
.catch((err: unknown) => {
|
|
315
|
+
logger.warn(
|
|
316
|
+
{ err: (err as Error)?.message ?? String(err) },
|
|
317
|
+
"telemetry.start.failed"
|
|
318
|
+
);
|
|
319
|
+
})
|
|
320
|
+
: Promise.resolve();
|
|
321
|
+
const finishTelemetry = (
|
|
322
|
+
status: "success" | "failed" | "skipped",
|
|
323
|
+
stopReason?: string,
|
|
324
|
+
errorMessage?: string
|
|
325
|
+
): void => {
|
|
326
|
+
if (!telemetry) return;
|
|
327
|
+
void telemetryStartFired.then(() =>
|
|
328
|
+
telemetry
|
|
329
|
+
.finish(runId, {
|
|
330
|
+
status,
|
|
331
|
+
stopReason,
|
|
332
|
+
errorMessage,
|
|
333
|
+
finishedAt: new Date().toISOString(),
|
|
334
|
+
})
|
|
335
|
+
.catch((err: unknown) =>
|
|
336
|
+
logger.warn(
|
|
337
|
+
{ err: (err as Error)?.message ?? String(err) },
|
|
338
|
+
"telemetry.finish.failed"
|
|
339
|
+
)
|
|
340
|
+
)
|
|
341
|
+
);
|
|
342
|
+
};
|
|
298
343
|
|
|
299
344
|
// ── OTel: agent-level span for the full cycle ───────────────────────────
|
|
300
345
|
const tracer = getTracer("vauban-agent-sdk.ooda");
|
|
@@ -369,6 +414,7 @@ export class OODAAgentImpl<
|
|
|
369
414
|
rootSpan.end();
|
|
370
415
|
this._cyclesCompleted += 1;
|
|
371
416
|
this._lastCycleAt = new Date().toISOString();
|
|
417
|
+
finishTelemetry("skipped", sessionResult.reason ?? "session_guard");
|
|
372
418
|
return { runId, status: "skipped" };
|
|
373
419
|
}
|
|
374
420
|
|
|
@@ -391,6 +437,7 @@ export class OODAAgentImpl<
|
|
|
391
437
|
rootSpan.end();
|
|
392
438
|
this._cyclesCompleted += 1;
|
|
393
439
|
this._lastCycleAt = new Date().toISOString();
|
|
440
|
+
finishTelemetry("skipped", riskResult.reason ?? "risk_guard");
|
|
394
441
|
return { runId, status: "skipped" };
|
|
395
442
|
}
|
|
396
443
|
|
|
@@ -410,6 +457,7 @@ export class OODAAgentImpl<
|
|
|
410
457
|
rootSpan.end();
|
|
411
458
|
this._cyclesCompleted += 1;
|
|
412
459
|
this._lastCycleAt = new Date().toISOString();
|
|
460
|
+
finishTelemetry("skipped", `heap_exceeded:${heapMb.toFixed(1)}mb`);
|
|
413
461
|
return { runId, status: "skipped" };
|
|
414
462
|
}
|
|
415
463
|
|
|
@@ -489,6 +537,7 @@ export class OODAAgentImpl<
|
|
|
489
537
|
rootSpan.end();
|
|
490
538
|
this._cyclesCompleted += 1;
|
|
491
539
|
this._lastCycleAt = new Date().toISOString();
|
|
540
|
+
finishTelemetry("success");
|
|
492
541
|
return { runId, status: "succeeded" };
|
|
493
542
|
} catch (err) {
|
|
494
543
|
logger.error(
|
|
@@ -509,6 +558,7 @@ export class OODAAgentImpl<
|
|
|
509
558
|
rootSpan.end();
|
|
510
559
|
this._cyclesCompleted += 1;
|
|
511
560
|
this._lastCycleAt = new Date().toISOString();
|
|
561
|
+
finishTelemetry("failed", undefined, (err as Error).message);
|
|
512
562
|
return { runId, status: "failed" };
|
|
513
563
|
}
|
|
514
564
|
}
|
|
@@ -282,6 +282,18 @@ export interface OODAAgentConfig<
|
|
|
282
282
|
readonly errorStepImpl?: OODAContext["errorStep"];
|
|
283
283
|
readonly notifySlackImpl?: OODAContext["notifySlack"];
|
|
284
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Optional telemetry sink (or a {@link createTelemetryBus} for multi-sink).
|
|
287
|
+
*
|
|
288
|
+
* Per ADR-ECO-039. Receives one `start`, 0..N `step`, and one `finish` event
|
|
289
|
+
* per OODA cycle. Failures are isolated by the bus — never block the agent.
|
|
290
|
+
* When absent, telemetry is silently skipped.
|
|
291
|
+
*
|
|
292
|
+
* @experimental — public-experimental contract (interface stable as of 1.3.0,
|
|
293
|
+
* additional event fields may be added before 2.0).
|
|
294
|
+
*/
|
|
295
|
+
readonly telemetry?: import("../../telemetry/port.js").TelemetrySink;
|
|
296
|
+
|
|
285
297
|
/**
|
|
286
298
|
* Optional HITL gate awaiter. Called when `act` phase has
|
|
287
299
|
* `hitlGate: true`. Resolves when human approves; rejects to abort.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal helper for skills that read configuration secrets.
|
|
3
|
+
*
|
|
4
|
+
* Prefers `ctx.secrets` (audit-trail-bearing per ADR-ECO-036) over
|
|
5
|
+
* `process.env` when a {@link SecretsAccessor} is configured on the
|
|
6
|
+
* {@link SkillContext}. Falls back to env when no accessor is wired so
|
|
7
|
+
* legacy callers see no behavior change.
|
|
8
|
+
*
|
|
9
|
+
* Returns `undefined` when the secret is configured nowhere — callers
|
|
10
|
+
* decide whether that's a SkillNotConfiguredError or a soft default.
|
|
11
|
+
*
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { SkillContext } from "../orchestration/ooda/skills.js";
|
|
16
|
+
|
|
17
|
+
export function readSecret(
|
|
18
|
+
ctx: SkillContext,
|
|
19
|
+
name: string
|
|
20
|
+
): string | undefined {
|
|
21
|
+
if (ctx.secrets && ctx.secrets.has(name)) {
|
|
22
|
+
return ctx.secrets.get(name);
|
|
23
|
+
}
|
|
24
|
+
return process.env[name];
|
|
25
|
+
}
|
|
@@ -6,9 +6,31 @@
|
|
|
6
6
|
* @public
|
|
7
7
|
*/
|
|
8
8
|
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
retry,
|
|
12
|
+
RETRY_TRANSIENT,
|
|
13
|
+
RetryExhaustedError,
|
|
14
|
+
type RetryConfig,
|
|
15
|
+
} from "../retry/index.js";
|
|
9
16
|
import type { Skill, SkillContext } from "../orchestration/ooda/skills.js";
|
|
17
|
+
|
|
10
18
|
import { SkillExecutionError, SkillNotConfiguredError } from "./errors.js";
|
|
11
19
|
import { withSkillSpan } from "./_otel.js";
|
|
20
|
+
import { readSecret } from "./_secrets.js";
|
|
21
|
+
|
|
22
|
+
class RetryableHttpError extends Error {
|
|
23
|
+
constructor(public readonly status: number, public readonly body: string) {
|
|
24
|
+
super(`HTTP ${status}: ${body.slice(0, 200)}`);
|
|
25
|
+
this.name = "RetryableHttpError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const ALPACA_RETRY: RetryConfig = {
|
|
30
|
+
...RETRY_TRANSIENT,
|
|
31
|
+
retryIf: (err) =>
|
|
32
|
+
err instanceof RetryableHttpError || err instanceof TypeError,
|
|
33
|
+
};
|
|
12
34
|
|
|
13
35
|
const inputSchema = z
|
|
14
36
|
.object({
|
|
@@ -46,8 +68,8 @@ export const alpacaQuote: Skill<AlpacaQuoteInput, AlpacaQuoteOutput> = {
|
|
|
46
68
|
};
|
|
47
69
|
}
|
|
48
70
|
return withSkillSpan("alpaca_quote", async () => {
|
|
49
|
-
const apiKey =
|
|
50
|
-
const apiSecret =
|
|
71
|
+
const apiKey = readSecret(ctx, "ALPACA_API_KEY");
|
|
72
|
+
const apiSecret = readSecret(ctx, "ALPACA_API_SECRET");
|
|
51
73
|
if (!apiKey || !apiSecret) {
|
|
52
74
|
throw new SkillNotConfiguredError("alpaca_quote", [
|
|
53
75
|
"ALPACA_API_KEY",
|
|
@@ -55,28 +77,51 @@ export const alpacaQuote: Skill<AlpacaQuoteInput, AlpacaQuoteOutput> = {
|
|
|
55
77
|
]);
|
|
56
78
|
}
|
|
57
79
|
const dataHost = "https://data.alpaca.markets";
|
|
58
|
-
const url = `${dataHost}/v2/stocks/${encodeURIComponent(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
80
|
+
const url = `${dataHost}/v2/stocks/${encodeURIComponent(
|
|
81
|
+
input.symbol
|
|
82
|
+
)}/quotes/latest`;
|
|
83
|
+
try {
|
|
84
|
+
return await retry(
|
|
85
|
+
async () => {
|
|
86
|
+
const res = await fetch(url, {
|
|
87
|
+
headers: {
|
|
88
|
+
"APCA-API-KEY-ID": apiKey,
|
|
89
|
+
"APCA-API-SECRET-KEY": apiSecret,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
const body = await res.text().catch(() => "");
|
|
94
|
+
if (res.status >= 500 || res.status === 429) {
|
|
95
|
+
throw new RetryableHttpError(res.status, body);
|
|
96
|
+
}
|
|
97
|
+
throw new SkillExecutionError("alpaca_quote", `${res.status}`, {
|
|
98
|
+
status: res.status,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const data = (await res.json()) as {
|
|
102
|
+
quote?: { bp?: number; ap?: number; t?: string };
|
|
103
|
+
};
|
|
104
|
+
return {
|
|
105
|
+
symbol: input.symbol,
|
|
106
|
+
bid: data.quote?.bp ?? 0,
|
|
107
|
+
ask: data.quote?.ap ?? 0,
|
|
108
|
+
timestamp: data.quote?.t ?? new Date().toISOString(),
|
|
109
|
+
mode: input.mode,
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
{ config: ALPACA_RETRY }
|
|
113
|
+
);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
const cause = err instanceof RetryExhaustedError ? err.lastError : err;
|
|
116
|
+
if (cause instanceof RetryableHttpError) {
|
|
117
|
+
throw new SkillExecutionError(
|
|
118
|
+
"alpaca_quote",
|
|
119
|
+
`${cause.status} after retries`,
|
|
120
|
+
{ status: cause.status }
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
throw cause;
|
|
69
124
|
}
|
|
70
|
-
const data = (await res.json()) as {
|
|
71
|
-
quote?: { bp?: number; ap?: number; t?: string };
|
|
72
|
-
};
|
|
73
|
-
return {
|
|
74
|
-
symbol: input.symbol,
|
|
75
|
-
bid: data.quote?.bp ?? 0,
|
|
76
|
-
ask: data.quote?.ap ?? 0,
|
|
77
|
-
timestamp: data.quote?.t ?? new Date().toISOString(),
|
|
78
|
-
mode: input.mode,
|
|
79
|
-
};
|
|
80
125
|
});
|
|
81
126
|
},
|
|
82
127
|
};
|
package/src/skills/send-email.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { z } from "zod";
|
|
|
11
11
|
import type { Skill, SkillContext } from "../orchestration/ooda/skills.js";
|
|
12
12
|
import { SkillExecutionError, SkillNotConfiguredError } from "./errors.js";
|
|
13
13
|
import { withSkillSpan } from "./_otel.js";
|
|
14
|
+
import { readSecret } from "./_secrets.js";
|
|
14
15
|
|
|
15
16
|
const inputSchema = z
|
|
16
17
|
.object({
|
|
@@ -29,18 +30,6 @@ export interface SendEmailOutput {
|
|
|
29
30
|
message_id: string | null;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
/**
|
|
33
|
-
* Prefer ctx.secrets (audit-trail-bearing) over process.env. When no secrets
|
|
34
|
-
* accessor is configured, falls back to env so legacy callers see no behavior
|
|
35
|
-
* change. When the secret is absent in both, returns undefined.
|
|
36
|
-
*/
|
|
37
|
-
function readSecret(ctx: SkillContext, name: string): string | undefined {
|
|
38
|
-
if (ctx.secrets) {
|
|
39
|
-
if (ctx.secrets.has(name)) return ctx.secrets.get(name);
|
|
40
|
-
}
|
|
41
|
-
return process.env[name];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
33
|
export const sendEmail: Skill<SendEmailInput, SendEmailOutput> = {
|
|
45
34
|
name: "send_email",
|
|
46
35
|
inputSchema,
|
|
@@ -6,9 +6,31 @@
|
|
|
6
6
|
* @public
|
|
7
7
|
*/
|
|
8
8
|
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
retry,
|
|
12
|
+
RETRY_TRANSIENT,
|
|
13
|
+
RetryExhaustedError,
|
|
14
|
+
type RetryConfig,
|
|
15
|
+
} from "../retry/index.js";
|
|
9
16
|
import type { Skill, SkillContext } from "../orchestration/ooda/skills.js";
|
|
17
|
+
|
|
10
18
|
import { SkillExecutionError, SkillNotConfiguredError } from "./errors.js";
|
|
11
19
|
import { withSkillSpan } from "./_otel.js";
|
|
20
|
+
import { readSecret } from "./_secrets.js";
|
|
21
|
+
|
|
22
|
+
class RetryableHttpError extends Error {
|
|
23
|
+
constructor(public readonly status: number, public readonly body: string) {
|
|
24
|
+
super(`HTTP ${status}: ${body.slice(0, 200)}`);
|
|
25
|
+
this.name = "RetryableHttpError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const SLACK_RETRY: RetryConfig = {
|
|
30
|
+
...RETRY_TRANSIENT,
|
|
31
|
+
retryIf: (err) =>
|
|
32
|
+
err instanceof RetryableHttpError || err instanceof TypeError,
|
|
33
|
+
};
|
|
12
34
|
|
|
13
35
|
const inputSchema = z
|
|
14
36
|
.object({
|
|
@@ -38,7 +60,7 @@ export const slackNotify: Skill<SlackNotifyInput, SlackNotifyOutput> = {
|
|
|
38
60
|
return { delivered: false, ts: null };
|
|
39
61
|
}
|
|
40
62
|
return withSkillSpan("slack_notify", async () => {
|
|
41
|
-
const token =
|
|
63
|
+
const token = readSecret(ctx, "SLACK_BOT_TOKEN");
|
|
42
64
|
if (!token) {
|
|
43
65
|
throw new SkillNotConfiguredError("slack_notify", ["SLACK_BOT_TOKEN"]);
|
|
44
66
|
}
|
|
@@ -46,45 +68,66 @@ export const slackNotify: Skill<SlackNotifyInput, SlackNotifyOutput> = {
|
|
|
46
68
|
if (!channel && input.binding_user_id) {
|
|
47
69
|
const { rows } = await ctx.db.query<{ chat_id: string }>(
|
|
48
70
|
"SELECT chat_id FROM messaging_user_binding WHERE user_id = $1 AND channel = 'slack' LIMIT 1",
|
|
49
|
-
[input.binding_user_id]
|
|
71
|
+
[input.binding_user_id]
|
|
50
72
|
);
|
|
51
73
|
channel = rows[0]?.chat_id;
|
|
52
74
|
if (!channel) {
|
|
53
75
|
throw new SkillExecutionError(
|
|
54
76
|
"slack_notify",
|
|
55
|
-
`no slack binding for user ${input.binding_user_id}
|
|
77
|
+
`no slack binding for user ${input.binding_user_id}`
|
|
56
78
|
);
|
|
57
79
|
}
|
|
58
80
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
try {
|
|
82
|
+
return await retry(
|
|
83
|
+
async () => {
|
|
84
|
+
const res = await fetch("https://slack.com/api/chat.postMessage", {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: {
|
|
87
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
88
|
+
Authorization: `Bearer ${token}`,
|
|
89
|
+
},
|
|
90
|
+
body: JSON.stringify({
|
|
91
|
+
channel,
|
|
92
|
+
text: input.text,
|
|
93
|
+
blocks: input.blocks,
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
if (!res.ok) {
|
|
97
|
+
const body = await res.text().catch(() => "");
|
|
98
|
+
if (res.status >= 500 || res.status === 429) {
|
|
99
|
+
throw new RetryableHttpError(res.status, body);
|
|
100
|
+
}
|
|
101
|
+
throw new SkillExecutionError("slack_notify", `${res.status}`, {
|
|
102
|
+
status: res.status,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const data = (await res.json()) as {
|
|
106
|
+
ok?: boolean;
|
|
107
|
+
ts?: string;
|
|
108
|
+
error?: string;
|
|
109
|
+
};
|
|
110
|
+
if (!data.ok) {
|
|
111
|
+
throw new SkillExecutionError(
|
|
112
|
+
"slack_notify",
|
|
113
|
+
data.error ?? "unknown slack error"
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return { delivered: true, ts: data.ts ?? null };
|
|
117
|
+
},
|
|
118
|
+
{ config: SLACK_RETRY }
|
|
85
119
|
);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
const cause = err instanceof RetryExhaustedError ? err.lastError : err;
|
|
122
|
+
if (cause instanceof RetryableHttpError) {
|
|
123
|
+
throw new SkillExecutionError(
|
|
124
|
+
"slack_notify",
|
|
125
|
+
`${cause.status} after retries`,
|
|
126
|
+
{ status: cause.status }
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
throw cause;
|
|
86
130
|
}
|
|
87
|
-
return { delivered: true, ts: data.ts ?? null };
|
|
88
131
|
});
|
|
89
132
|
},
|
|
90
133
|
};
|
|
@@ -7,9 +7,31 @@
|
|
|
7
7
|
* @public
|
|
8
8
|
*/
|
|
9
9
|
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
retry,
|
|
13
|
+
RETRY_TRANSIENT,
|
|
14
|
+
RetryExhaustedError,
|
|
15
|
+
type RetryConfig,
|
|
16
|
+
} from "../retry/index.js";
|
|
10
17
|
import type { Skill, SkillContext } from "../orchestration/ooda/skills.js";
|
|
18
|
+
|
|
11
19
|
import { SkillExecutionError, SkillNotConfiguredError } from "./errors.js";
|
|
12
20
|
import { withSkillSpan } from "./_otel.js";
|
|
21
|
+
import { readSecret } from "./_secrets.js";
|
|
22
|
+
|
|
23
|
+
class RetryableHttpError extends Error {
|
|
24
|
+
constructor(public readonly status: number, public readonly body: string) {
|
|
25
|
+
super(`HTTP ${status}: ${body.slice(0, 200)}`);
|
|
26
|
+
this.name = "RetryableHttpError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const TELEGRAM_RETRY: RetryConfig = {
|
|
31
|
+
...RETRY_TRANSIENT,
|
|
32
|
+
retryIf: (err) =>
|
|
33
|
+
err instanceof RetryableHttpError || err instanceof TypeError,
|
|
34
|
+
};
|
|
13
35
|
|
|
14
36
|
const inputSchema = z
|
|
15
37
|
.object({
|
|
@@ -40,7 +62,7 @@ export const telegramNotify: Skill<TelegramNotifyInput, TelegramNotifyOutput> =
|
|
|
40
62
|
return { delivered: false, message_id: null };
|
|
41
63
|
}
|
|
42
64
|
return withSkillSpan("telegram_notify", async () => {
|
|
43
|
-
const token =
|
|
65
|
+
const token = readSecret(ctx, "TELEGRAM_BOT_TOKEN");
|
|
44
66
|
if (!token) {
|
|
45
67
|
throw new SkillNotConfiguredError("telegram_notify", [
|
|
46
68
|
"TELEGRAM_BOT_TOKEN",
|
|
@@ -50,39 +72,63 @@ export const telegramNotify: Skill<TelegramNotifyInput, TelegramNotifyOutput> =
|
|
|
50
72
|
if (!chatId && input.binding_user_id) {
|
|
51
73
|
const { rows } = await ctx.db.query<{ chat_id: string }>(
|
|
52
74
|
"SELECT chat_id FROM messaging_user_binding WHERE user_id = $1 AND channel = 'telegram' LIMIT 1",
|
|
53
|
-
[input.binding_user_id]
|
|
75
|
+
[input.binding_user_id]
|
|
54
76
|
);
|
|
55
77
|
chatId = rows[0]?.chat_id;
|
|
56
78
|
if (!chatId) {
|
|
57
79
|
throw new SkillExecutionError(
|
|
58
80
|
"telegram_notify",
|
|
59
|
-
`no telegram binding for user ${input.binding_user_id}
|
|
81
|
+
`no telegram binding for user ${input.binding_user_id}`
|
|
60
82
|
);
|
|
61
83
|
}
|
|
62
84
|
}
|
|
63
85
|
const url = `https://api.telegram.org/bot${token}/sendMessage`;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
86
|
+
try {
|
|
87
|
+
return await retry(
|
|
88
|
+
async () => {
|
|
89
|
+
const res = await fetch(url, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: { "Content-Type": "application/json" },
|
|
92
|
+
body: JSON.stringify({
|
|
93
|
+
chat_id: chatId,
|
|
94
|
+
text: input.text,
|
|
95
|
+
parse_mode: input.parse_mode,
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
if (!res.ok) {
|
|
99
|
+
const body = await res.text().catch(() => "");
|
|
100
|
+
if (res.status >= 500 || res.status === 429) {
|
|
101
|
+
throw new RetryableHttpError(res.status, body);
|
|
102
|
+
}
|
|
103
|
+
throw new SkillExecutionError(
|
|
104
|
+
"telegram_notify",
|
|
105
|
+
`${res.status}`,
|
|
106
|
+
{ status: res.status }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const data = (await res.json()) as {
|
|
110
|
+
ok?: boolean;
|
|
111
|
+
result?: { message_id?: number };
|
|
112
|
+
};
|
|
113
|
+
return {
|
|
114
|
+
delivered: data.ok === true,
|
|
115
|
+
message_id: data.result?.message_id ?? null,
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
{ config: TELEGRAM_RETRY }
|
|
119
|
+
);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
const cause =
|
|
122
|
+
err instanceof RetryExhaustedError ? err.lastError : err;
|
|
123
|
+
if (cause instanceof RetryableHttpError) {
|
|
124
|
+
throw new SkillExecutionError(
|
|
125
|
+
"telegram_notify",
|
|
126
|
+
`${cause.status} after retries`,
|
|
127
|
+
{ status: cause.status }
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
throw cause;
|
|
77
131
|
}
|
|
78
|
-
const data = (await res.json()) as {
|
|
79
|
-
ok?: boolean;
|
|
80
|
-
result?: { message_id?: number };
|
|
81
|
-
};
|
|
82
|
-
return {
|
|
83
|
-
delivered: data.ok === true,
|
|
84
|
-
message_id: data.result?.message_id ?? null,
|
|
85
|
-
};
|
|
86
132
|
});
|
|
87
133
|
},
|
|
88
134
|
};
|