@hivemagnus/ardent 0.0.0-stage → 1.0.2
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 +202 -0
- package/README.md +274 -2
- package/dist/a2a-client.d.ts +35 -0
- package/dist/a2a-client.js +172 -0
- package/dist/a2a-hermes-alias.d.ts +3 -0
- package/dist/a2a-hermes-alias.js +57 -0
- package/dist/a2a-idempotency.d.ts +13 -0
- package/dist/a2a-idempotency.js +86 -0
- package/dist/a2a-peers.d.ts +28 -0
- package/dist/a2a-peers.js +108 -0
- package/dist/a2a-push-inbox.d.ts +22 -0
- package/dist/a2a-push-inbox.js +84 -0
- package/dist/a2a-push.d.ts +27 -0
- package/dist/a2a-push.js +188 -0
- package/dist/a2a-server.d.ts +37 -0
- package/dist/a2a-server.js +186 -0
- package/dist/a2a-store.d.ts +10 -0
- package/dist/a2a-store.js +122 -0
- package/dist/a2a.d.ts +19 -0
- package/dist/a2a.js +466 -0
- package/dist/api.d.ts +6 -0
- package/dist/api.js +381 -0
- package/dist/artifacts.d.ts +15 -0
- package/dist/artifacts.js +41 -0
- package/dist/backend-events.d.ts +49 -0
- package/dist/backend-events.js +8 -0
- package/dist/channels.d.ts +52 -0
- package/dist/channels.js +184 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +845 -0
- package/dist/codex-output-budget.d.ts +17 -0
- package/dist/codex-output-budget.js +31 -0
- package/dist/codex-output-budget.test.d.ts +1 -0
- package/dist/codex-output-budget.test.js +29 -0
- package/dist/codex.d.ts +82 -0
- package/dist/codex.js +431 -0
- package/dist/config.d.ts +171 -0
- package/dist/config.js +244 -0
- package/dist/context-checkpoints.d.ts +49 -0
- package/dist/context-checkpoints.js +248 -0
- package/dist/context.d.ts +16 -0
- package/dist/context.js +101 -0
- package/dist/gateway-lock.d.ts +18 -0
- package/dist/gateway-lock.js +104 -0
- package/dist/hermes-compat.d.ts +15 -0
- package/dist/hermes-compat.js +62 -0
- package/dist/identity.d.ts +15 -0
- package/dist/identity.js +52 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -0
- package/dist/job-engine.d.ts +222 -0
- package/dist/job-engine.js +497 -0
- package/dist/job-news-handlers.d.ts +54 -0
- package/dist/job-news-handlers.js +153 -0
- package/dist/job-ownership.d.ts +9 -0
- package/dist/job-ownership.js +17 -0
- package/dist/json-schema.d.ts +9 -0
- package/dist/json-schema.js +20 -0
- package/dist/llamacpp.d.ts +49 -0
- package/dist/llamacpp.js +216 -0
- package/dist/magnus-control.d.ts +3 -0
- package/dist/magnus-control.js +99 -0
- package/dist/magnus-node-dependency-impact.d.ts +1 -0
- package/dist/magnus-node-dependency-impact.js +126 -0
- package/dist/magnus-owner-reads.d.ts +4 -0
- package/dist/magnus-owner-reads.js +99 -0
- package/dist/magnus-readiness.d.ts +1 -0
- package/dist/magnus-readiness.js +108 -0
- package/dist/mattermost.d.ts +18 -0
- package/dist/mattermost.js +303 -0
- package/dist/memory-tools.d.ts +5 -0
- package/dist/memory-tools.js +49 -0
- package/dist/memory.d.ts +40 -0
- package/dist/memory.js +128 -0
- package/dist/model-policy.d.ts +64 -0
- package/dist/model-policy.js +56 -0
- package/dist/news.d.ts +10 -0
- package/dist/news.js +291 -0
- package/dist/owner-read.d.ts +32 -0
- package/dist/owner-read.js +130 -0
- package/dist/plugins.d.ts +74 -0
- package/dist/plugins.js +115 -0
- package/dist/research.d.ts +110 -0
- package/dist/research.js +164 -0
- package/dist/runtime.d.ts +33 -0
- package/dist/runtime.js +665 -0
- package/dist/skills.d.ts +13 -0
- package/dist/skills.js +87 -0
- package/dist/store.d.ts +142 -0
- package/dist/store.js +322 -0
- package/dist/team-artifacts.d.ts +138 -0
- package/dist/team-artifacts.js +241 -0
- package/dist/team-child.d.ts +21 -0
- package/dist/team-child.js +130 -0
- package/dist/team-contract.d.ts +116 -0
- package/dist/team-contract.js +84 -0
- package/dist/team-control.d.ts +8 -0
- package/dist/team-control.js +152 -0
- package/dist/telegram.d.ts +35 -0
- package/dist/telegram.js +284 -0
- package/dist/tools.d.ts +48 -0
- package/dist/tools.js +58 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +5 -0
- package/model-policy.default.json +20 -0
- package/package.json +84 -4
- package/profiles/assistant/IDENTITY.md +9 -0
- package/profiles/assistant/SOUL.md +8 -0
- package/profiles/assistant/config.yaml +88 -0
- package/profiles/assistant/peers.yaml +12 -0
- package/profiles/assistant/skills/grounded-answers/SKILL.md +18 -0
- package/protocol/LICENSE-A2A.txt +202 -0
- package/protocol/README.md +12 -0
- package/protocol/a2a.proto +811 -0
- package/protocol/lock.json +9 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const TEAM_VERSION = '1.0';
|
|
3
|
+
export const TEAM_TASK_METADATA_KEY = 'hermesTeamTask';
|
|
4
|
+
export const TEAM_CLARIFICATION_METADATA_KEY = 'hermesTeamClarification';
|
|
5
|
+
/** A configured peer ID (see peers.yaml). */
|
|
6
|
+
const role = z.string().regex(/^[a-z][a-z0-9_-]{0,63}$/);
|
|
7
|
+
const boundedId = z.string().regex(/^[A-Za-z0-9._:-]{1,128}$/);
|
|
8
|
+
const text = (bytes) => z.string().trim().min(1).refine(value => Buffer.byteLength(value, 'utf8') <= bytes, `Exceeds ${bytes} bytes`);
|
|
9
|
+
const texts = (minimum = 0) => z.array(text(1000)).min(minimum).max(32).refine(value => new Set(value).size === value.length, 'Duplicate entries');
|
|
10
|
+
const Action = z.enum(['observe', 'analyze', 'create_artifact', 'create_draft', 'reversible_change', 'deploy', 'external_publish', 'destructive_response']);
|
|
11
|
+
/** The mission shape without its cross-field rules (Zod 4 cannot omit fields from a refined object). */
|
|
12
|
+
export const TeamMissionShape = z.object({
|
|
13
|
+
id: boundedId,
|
|
14
|
+
revision: z.number().int().min(1).max(2147483647),
|
|
15
|
+
parent_task: boundedId.nullable(),
|
|
16
|
+
constraints: texts(),
|
|
17
|
+
allowed_actions: z.array(Action).min(1).max(8).refine(value => new Set(value).size === value.length, 'Duplicate action'),
|
|
18
|
+
resources: texts(1),
|
|
19
|
+
deadline: z.number().int().min(1).max(253402300799),
|
|
20
|
+
acceptance_criteria: texts(1),
|
|
21
|
+
expected_artifacts: texts()
|
|
22
|
+
}).strict();
|
|
23
|
+
export const TeamMission = TeamMissionShape;
|
|
24
|
+
export const TeamEnvelope = z.object({ version: z.literal(TEAM_VERSION), caller: role, task_id: boundedId, objective: text(16000), mission: TeamMission }).strict();
|
|
25
|
+
const Clarification = z.object({ missing_fact: text(1200), choices: z.array(z.object({ id: boundedId, label: text(300) }).strict()).max(8), blocked_task: boundedId, intended_respondent: z.union([role, z.literal('operator')]) }).strict();
|
|
26
|
+
const Check = z.object({ criterion: text(1000), status: z.enum(['pass', 'fail', 'unknown']), evidence: z.array(text(1000)).max(16) }).strict();
|
|
27
|
+
const Artifact = z.object({ name: text(1000), reference: text(1000), sha256: z.string().regex(/^[a-f0-9]{64}$/) }).strict();
|
|
28
|
+
export const TeamResult = z.object({
|
|
29
|
+
version: z.literal(TEAM_VERSION), mission_id: boundedId, mission_revision: z.number().int().min(1), task_id: boundedId,
|
|
30
|
+
outcome: z.enum(['completed', 'partial', 'failed', 'blocked', 'input_required', 'cancelled', 'unknown']),
|
|
31
|
+
summary: text(16000), checks: z.array(Check).max(32), artifacts: z.array(Artifact).max(32), uncertain_effects: texts(),
|
|
32
|
+
clarification: Clarification.optional()
|
|
33
|
+
}).strict();
|
|
34
|
+
export function validateTeamEnvelope(value, authenticatedPeer, messageId, objective, nowSeconds = Date.now() / 1000, child = false) {
|
|
35
|
+
const envelope = TeamEnvelope.parse(value);
|
|
36
|
+
if (envelope.caller !== authenticatedPeer || envelope.task_id !== messageId || envelope.objective !== objective)
|
|
37
|
+
throw new Error('Team envelope does not match authenticated message');
|
|
38
|
+
if (child ? envelope.mission.parent_task === null : envelope.mission.parent_task !== null)
|
|
39
|
+
throw new Error('Child mission requires an exact retained root reservation');
|
|
40
|
+
if (envelope.mission.deadline <= nowSeconds)
|
|
41
|
+
throw new Error('Team mission deadline expired');
|
|
42
|
+
if (Buffer.byteLength(JSON.stringify(envelope), 'utf8') > 48_000)
|
|
43
|
+
throw new Error('Team envelope exceeds 48 KiB');
|
|
44
|
+
return envelope;
|
|
45
|
+
}
|
|
46
|
+
export function validateTeamResult(raw, envelope, knownReferences) {
|
|
47
|
+
if (Buffer.byteLength(raw, 'utf8') > 262_144)
|
|
48
|
+
throw new Error('Team result exceeds 256 KiB');
|
|
49
|
+
let text = raw.trim().replace(/^\[INPUT_REQUIRED\]\s*/, '');
|
|
50
|
+
if (text.startsWith('```json\n') && text.endsWith('\n```'))
|
|
51
|
+
text = text.slice(8, -4);
|
|
52
|
+
let value;
|
|
53
|
+
try {
|
|
54
|
+
value = JSON.parse(text);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw new Error('A structured JSON team result is required');
|
|
58
|
+
}
|
|
59
|
+
const result = TeamResult.parse(value);
|
|
60
|
+
if (result.mission_id !== envelope.mission.id || result.mission_revision !== envelope.mission.revision || result.task_id !== envelope.task_id)
|
|
61
|
+
throw new Error('Team result mission or task binding differs');
|
|
62
|
+
if (result.clarification && (result.outcome !== 'input_required' || result.clarification.blocked_task !== envelope.task_id))
|
|
63
|
+
throw new Error('Invalid team clarification binding');
|
|
64
|
+
const criteria = new Set(envelope.mission.acceptance_criteria);
|
|
65
|
+
const seen = new Set();
|
|
66
|
+
for (const check of result.checks) {
|
|
67
|
+
if (!criteria.has(check.criterion) || seen.has(check.criterion))
|
|
68
|
+
throw new Error('Unknown or duplicate team check');
|
|
69
|
+
if (knownReferences && check.evidence.some(reference => !knownReferences.has(reference)))
|
|
70
|
+
throw new Error('Team check cites an unobserved evidence reference');
|
|
71
|
+
seen.add(check.criterion);
|
|
72
|
+
}
|
|
73
|
+
const names = new Set();
|
|
74
|
+
for (const artifact of result.artifacts) {
|
|
75
|
+
if (names.has(artifact.name))
|
|
76
|
+
throw new Error('Duplicate team artifact');
|
|
77
|
+
names.add(artifact.name);
|
|
78
|
+
}
|
|
79
|
+
if (result.outcome === 'completed' &&
|
|
80
|
+
(seen.size !== criteria.size || result.checks.some(check => check.status !== 'pass' || !check.evidence.length) ||
|
|
81
|
+
envelope.mission.expected_artifacts.some(name => !names.has(name)) || result.uncertain_effects.length))
|
|
82
|
+
throw new Error('Team completion lacks checks, artifacts, or effect certainty');
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ArdentRuntime } from './runtime.js';
|
|
2
|
+
/** Deterministic receiver-side lifecycle controls for an accepted team task. */
|
|
3
|
+
export declare class TeamControlService {
|
|
4
|
+
#private;
|
|
5
|
+
readonly runtime: ArdentRuntime;
|
|
6
|
+
constructor(runtime: ArdentRuntime);
|
|
7
|
+
handle(raw: unknown, tenant: string, caller: string): object;
|
|
8
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { TeamEnvelope } from './team-contract.js';
|
|
4
|
+
const id = z.string().regex(/^[A-Za-z0-9._:-]{1,128}$/);
|
|
5
|
+
const EventsPayload = z.object({ native_task_id: id, after_cursor: z.number().int().min(0), limit: z.number().int().min(1).max(100) }).strict();
|
|
6
|
+
const ControlPayload = z.object({ native_task_id: id, task_id: id, mission_revision: z.number().int().min(1), control_id: id, control_version: z.number().int().min(1), action: z.enum(['pause', 'resume', 'cancel']) }).strict();
|
|
7
|
+
const TeamControl = z.discriminatedUnion('action', [
|
|
8
|
+
z.object({ version: z.literal('1.0'), action: z.literal('task_events'), key: id, payload: EventsPayload }).strict(),
|
|
9
|
+
z.object({ version: z.literal('1.0'), action: z.literal('task_control'), key: id, payload: ControlPayload }).strict()
|
|
10
|
+
]);
|
|
11
|
+
const MissionRow = z.object({ envelope_json: z.string(), tenant: z.string(), caller: z.string(), operation_id: z.string() });
|
|
12
|
+
const RequestRow = z.object({ request_digest: z.string(), receipt_json: z.string() });
|
|
13
|
+
const AdmissionRow = z.object({ version: z.number(), admission: z.enum(['open', 'paused', 'cancel_requested']) });
|
|
14
|
+
const ControlRow = z.object({ payload_sha256: z.string(), receipt_json: z.string() });
|
|
15
|
+
const EventRow = z.object({ sequence: z.number(), kind: z.string(), detail_json: z.string(), created_ms: z.number() });
|
|
16
|
+
function canonical(value) {
|
|
17
|
+
const sort = (input) => {
|
|
18
|
+
if (Array.isArray(input))
|
|
19
|
+
return input.map(sort);
|
|
20
|
+
if (input && typeof input === 'object')
|
|
21
|
+
return Object.fromEntries(Object.entries(input).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, sort(item)]));
|
|
22
|
+
return input;
|
|
23
|
+
};
|
|
24
|
+
return JSON.stringify(sort(value));
|
|
25
|
+
}
|
|
26
|
+
function digest(value) { return createHash('sha256').update(canonical(value)).digest('hex'); }
|
|
27
|
+
/** Deterministic receiver-side lifecycle controls for an accepted team task. */
|
|
28
|
+
export class TeamControlService {
|
|
29
|
+
runtime;
|
|
30
|
+
constructor(runtime) {
|
|
31
|
+
this.runtime = runtime;
|
|
32
|
+
runtime.store.db.exec(`CREATE TABLE IF NOT EXISTS a2a_team_control_requests (
|
|
33
|
+
tenant TEXT NOT NULL, caller TEXT NOT NULL, request_key TEXT NOT NULL,
|
|
34
|
+
request_digest TEXT NOT NULL, receipt_json TEXT NOT NULL,
|
|
35
|
+
PRIMARY KEY(tenant,caller,request_key));
|
|
36
|
+
CREATE TABLE IF NOT EXISTS a2a_team_admission (
|
|
37
|
+
a2a_task_id TEXT PRIMARY KEY, operation_id TEXT NOT NULL, version INTEGER NOT NULL,
|
|
38
|
+
admission TEXT NOT NULL, updated_ms INTEGER NOT NULL);
|
|
39
|
+
CREATE TABLE IF NOT EXISTS a2a_team_controls (
|
|
40
|
+
a2a_task_id TEXT NOT NULL, control_id TEXT NOT NULL, payload_sha256 TEXT NOT NULL,
|
|
41
|
+
receipt_json TEXT NOT NULL, PRIMARY KEY(a2a_task_id,control_id));
|
|
42
|
+
CREATE TABLE IF NOT EXISTS a2a_operation_links (
|
|
43
|
+
a2a_task_id TEXT NOT NULL, operation_id TEXT NOT NULL,
|
|
44
|
+
PRIMARY KEY(a2a_task_id,operation_id));`);
|
|
45
|
+
}
|
|
46
|
+
#mission(taskId, tenant, caller) {
|
|
47
|
+
const row = this.runtime.store.db.prepare(`SELECT m.envelope_json,m.tenant,m.caller,o.operation_id FROM a2a_team_missions m
|
|
48
|
+
JOIN a2a_operation_map o ON o.a2a_task_id=m.a2a_task_id
|
|
49
|
+
JOIN a2a_tasks t ON t.id=m.a2a_task_id
|
|
50
|
+
WHERE m.a2a_task_id=? AND t.tenant=? AND t.caller=?`).get(taskId, tenant, caller);
|
|
51
|
+
if (!row)
|
|
52
|
+
throw new Error('Accepted team task is unavailable to this peer');
|
|
53
|
+
const bound = MissionRow.parse(row);
|
|
54
|
+
if (bound.tenant !== tenant || bound.caller !== caller)
|
|
55
|
+
throw new Error('Team task owner differs');
|
|
56
|
+
const raw = JSON.parse(bound.envelope_json);
|
|
57
|
+
return { envelope: TeamEnvelope.parse(raw), operationId: bound.operation_id };
|
|
58
|
+
}
|
|
59
|
+
#events(payload, tenant, caller) {
|
|
60
|
+
const { envelope } = this.#mission(payload.native_task_id, tenant, caller);
|
|
61
|
+
const rows = this.runtime.store.db.prepare(`SELECT e.sequence,e.kind,e.detail_json,e.created_ms FROM events e
|
|
62
|
+
JOIN a2a_operation_links l ON l.operation_id=e.task_id
|
|
63
|
+
WHERE l.a2a_task_id=? AND e.sequence>? ORDER BY e.sequence LIMIT ?`).all(payload.native_task_id, payload.after_cursor, payload.limit + 1)
|
|
64
|
+
.map(row => EventRow.parse(row));
|
|
65
|
+
const events = [];
|
|
66
|
+
let bytes = 0;
|
|
67
|
+
for (const row of rows.slice(0, payload.limit)) {
|
|
68
|
+
const detail = JSON.parse(row.detail_json);
|
|
69
|
+
let item = { cursor: row.sequence, task_id: payload.native_task_id, kind: row.kind,
|
|
70
|
+
stage: row.kind.startsWith('tool_') ? 'tool' : row.kind.startsWith('model_') ? 'model' : 'task',
|
|
71
|
+
status: 'observed', summary: row.kind.replaceAll('_', ' ').slice(0, 160), data: detail,
|
|
72
|
+
created: row.created_ms / 1000 };
|
|
73
|
+
if (Buffer.byteLength(canonical(item)) > 32_768)
|
|
74
|
+
item = { ...item, data: { truncated: true } };
|
|
75
|
+
const size = Buffer.byteLength(canonical(item));
|
|
76
|
+
if (events.length && bytes + size > 32_768)
|
|
77
|
+
break;
|
|
78
|
+
events.push(item);
|
|
79
|
+
bytes += size;
|
|
80
|
+
}
|
|
81
|
+
const last = events.at(-1);
|
|
82
|
+
const nextCursor = last?.cursor ?? payload.after_cursor;
|
|
83
|
+
return { native_task_id: payload.native_task_id, task_id: envelope.task_id, events,
|
|
84
|
+
next_cursor: nextCursor, has_more: rows.some(row => row.sequence > nextCursor) };
|
|
85
|
+
}
|
|
86
|
+
#control(payload, tenant, caller) {
|
|
87
|
+
const { envelope, operationId } = this.#mission(payload.native_task_id, tenant, caller);
|
|
88
|
+
if (envelope.task_id !== payload.task_id || envelope.mission.revision !== payload.mission_revision)
|
|
89
|
+
throw new Error('Team control task or mission revision differs');
|
|
90
|
+
const db = this.runtime.store.db;
|
|
91
|
+
return db.transaction(() => {
|
|
92
|
+
const priorRaw = db.prepare('SELECT payload_sha256,receipt_json FROM a2a_team_controls WHERE a2a_task_id=? AND control_id=?')
|
|
93
|
+
.get(payload.native_task_id, payload.control_id);
|
|
94
|
+
const fingerprint = digest(payload);
|
|
95
|
+
if (priorRaw) {
|
|
96
|
+
const prior = ControlRow.parse(priorRaw);
|
|
97
|
+
if (prior.payload_sha256 !== fingerprint)
|
|
98
|
+
throw new Error('Team control ID reused with different content');
|
|
99
|
+
const data = JSON.parse(prior.receipt_json);
|
|
100
|
+
return { data: z.record(z.string(), z.unknown()).parse(data), ...(payload.action === 'cancel' ? { cancelOperation: operationId } : {}) };
|
|
101
|
+
}
|
|
102
|
+
const currentRaw = db.prepare('SELECT version,admission FROM a2a_team_admission WHERE a2a_task_id=?').get(payload.native_task_id);
|
|
103
|
+
const current = currentRaw ? AdmissionRow.parse(currentRaw) : { version: 0, admission: 'open' };
|
|
104
|
+
if (payload.control_version <= current.version)
|
|
105
|
+
throw new Error('Stale team control version');
|
|
106
|
+
if (current.admission === 'cancel_requested' && payload.action !== 'cancel')
|
|
107
|
+
throw new Error('Cancellation admission cannot be reopened');
|
|
108
|
+
if (payload.action === 'resume' && envelope.mission.deadline <= Date.now() / 1000)
|
|
109
|
+
throw new Error('Expired team mission cannot resume');
|
|
110
|
+
const admission = payload.action === 'pause' ? 'paused' : payload.action === 'resume' ? 'open' : 'cancel_requested';
|
|
111
|
+
db.prepare(`INSERT INTO a2a_team_admission(a2a_task_id,operation_id,version,admission,updated_ms) VALUES (?,?,?,?,?)
|
|
112
|
+
ON CONFLICT(a2a_task_id) DO UPDATE SET operation_id=excluded.operation_id,version=excluded.version,admission=excluded.admission,updated_ms=excluded.updated_ms`)
|
|
113
|
+
.run(payload.native_task_id, operationId, payload.control_version, admission, Date.now());
|
|
114
|
+
const data = { control_id: payload.control_id, native_task_id: payload.native_task_id,
|
|
115
|
+
control_version: payload.control_version, admission, execution_stopped: false,
|
|
116
|
+
provider_work_stopped: false, undo: false, delivery: 'acknowledged' };
|
|
117
|
+
db.prepare('INSERT INTO a2a_team_controls(a2a_task_id,control_id,payload_sha256,receipt_json) VALUES (?,?,?,?)')
|
|
118
|
+
.run(payload.native_task_id, payload.control_id, fingerprint, canonical(data));
|
|
119
|
+
this.runtime.store.append(operationId, 'team_control', data);
|
|
120
|
+
return { data, ...(payload.action === 'cancel' ? { cancelOperation: operationId } : {}) };
|
|
121
|
+
})();
|
|
122
|
+
}
|
|
123
|
+
handle(raw, tenant, caller) {
|
|
124
|
+
const control = TeamControl.parse(raw);
|
|
125
|
+
const requestDigest = digest(control);
|
|
126
|
+
const db = this.runtime.store.db;
|
|
127
|
+
const priorRaw = db.prepare('SELECT request_digest,receipt_json FROM a2a_team_control_requests WHERE tenant=? AND caller=? AND request_key=?')
|
|
128
|
+
.get(tenant, caller, control.key);
|
|
129
|
+
if (priorRaw) {
|
|
130
|
+
const prior = RequestRow.parse(priorRaw);
|
|
131
|
+
if (prior.request_digest !== requestDigest)
|
|
132
|
+
throw new Error('Team control key reused with different content');
|
|
133
|
+
const stored = JSON.parse(prior.receipt_json);
|
|
134
|
+
// A crash between persisting a cancel receipt and signaling the local
|
|
135
|
+
// operation must not make a retried request a no-op.
|
|
136
|
+
if (control.action === 'task_control' && control.payload.action === 'cancel') {
|
|
137
|
+
const { operationId } = this.#mission(control.payload.native_task_id, tenant, caller);
|
|
138
|
+
this.runtime.cancel(operationId);
|
|
139
|
+
}
|
|
140
|
+
return z.record(z.string(), z.unknown()).parse(stored);
|
|
141
|
+
}
|
|
142
|
+
const result = control.action === 'task_events'
|
|
143
|
+
? { data: this.#events(control.payload, tenant, caller) }
|
|
144
|
+
: this.#control(control.payload, tenant, caller);
|
|
145
|
+
const receipt = { version: '1.0', request_digest: requestDigest, data: result.data };
|
|
146
|
+
db.prepare('INSERT INTO a2a_team_control_requests(tenant,caller,request_key,request_digest,receipt_json) VALUES (?,?,?,?,?)')
|
|
147
|
+
.run(tenant, caller, control.key, requestDigest, canonical(receipt));
|
|
148
|
+
if (result.cancelOperation)
|
|
149
|
+
this.runtime.cancel(result.cancelOperation);
|
|
150
|
+
return receipt;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ArdentRuntime } from './runtime.js';
|
|
2
|
+
import type { JobEngine } from './job-engine.js';
|
|
3
|
+
type TelegramConfig = ArdentRuntime['config']['channels']['telegram'];
|
|
4
|
+
export declare class TelegramConflictError extends Error {
|
|
5
|
+
}
|
|
6
|
+
export declare class TelegramAdapter {
|
|
7
|
+
#private;
|
|
8
|
+
readonly runtime: ArdentRuntime;
|
|
9
|
+
readonly config: TelegramConfig;
|
|
10
|
+
constructor(runtime: ArdentRuntime, config: TelegramConfig, env?: NodeJS.ProcessEnv, baseUrl?: string, jobs?: JobEngine);
|
|
11
|
+
initialize(signal?: AbortSignal): Promise<{
|
|
12
|
+
botId: number;
|
|
13
|
+
username: string | null;
|
|
14
|
+
}>;
|
|
15
|
+
/**
|
|
16
|
+
* Send one notice to an allowlisted private chat (for plugins and jobs).
|
|
17
|
+
* With `parseMode`, Telegram returns rendered text, so the receipt binds the
|
|
18
|
+
* submitted digest and provider message ID instead of the echoed text.
|
|
19
|
+
*/
|
|
20
|
+
sendNotice(chatId: number, body: string, signal?: AbortSignal, parseMode?: 'MarkdownV2' | 'HTML'): Promise<{
|
|
21
|
+
message_id: string;
|
|
22
|
+
chat_id: number;
|
|
23
|
+
chat_type: 'private';
|
|
24
|
+
body_sha256: string;
|
|
25
|
+
}>;
|
|
26
|
+
/** `signal` stops intake; `deliverySignal` aborts replies already admitted from this poll. */
|
|
27
|
+
pollOnce(signal?: AbortSignal, deliverySignal?: AbortSignal | undefined): Promise<number>;
|
|
28
|
+
drain(): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Polls until `signal` aborts, then waits for admitted updates. Their replies
|
|
31
|
+
* use `deliverySignal`, so a shutdown can stop intake before cutting delivery.
|
|
32
|
+
*/
|
|
33
|
+
run(signal: AbortSignal, deliverySignal?: AbortSignal): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
export {};
|
package/dist/telegram.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { ChannelCore, DeliveryRejectedError } from './channels.js';
|
|
5
|
+
const User = z.object({ id: z.number().int(), is_bot: z.boolean().default(false), username: z.string().optional() }).passthrough();
|
|
6
|
+
const Document = z.object({ file_id: z.string(), file_name: z.string().default('attachment'), mime_type: z.string().default('application/octet-stream'), file_size: z.number().int().nonnegative().optional() }).passthrough();
|
|
7
|
+
const Message = z.object({ message_id: z.number().int(), from: User.optional(), chat: z.object({ id: z.number().int(), type: z.string() }).passthrough(), text: z.string().optional(), caption: z.string().optional(), message_thread_id: z.number().int().optional(), reply_to_message: z.object({ message_id: z.number().int() }).passthrough().optional(), document: Document.optional() }).passthrough();
|
|
8
|
+
const Update = z.object({ update_id: z.number().int(), message: Message.optional() }).passthrough();
|
|
9
|
+
const Envelope = z.object({ ok: z.boolean(), result: z.unknown().optional(), error_code: z.number().int().optional(), description: z.string().optional(), parameters: z.object({ retry_after: z.number().int().positive().optional() }).passthrough().optional() }).passthrough();
|
|
10
|
+
export class TelegramConflictError extends Error {
|
|
11
|
+
}
|
|
12
|
+
function splitText(value) {
|
|
13
|
+
const parts = [];
|
|
14
|
+
let rest = value || '(empty response)';
|
|
15
|
+
while (rest.length) {
|
|
16
|
+
if (rest.length <= 3900) {
|
|
17
|
+
parts.push(rest);
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
let index = rest.lastIndexOf('\n', 3900);
|
|
21
|
+
if (index < 2000)
|
|
22
|
+
index = rest.lastIndexOf(' ', 3900);
|
|
23
|
+
if (index < 2000)
|
|
24
|
+
index = 3900;
|
|
25
|
+
if (index > 0 && /[\uD800-\uDBFF]/.test(rest[index - 1] ?? '') && /[\uDC00-\uDFFF]/.test(rest[index] ?? ''))
|
|
26
|
+
index--;
|
|
27
|
+
parts.push(rest.slice(0, index));
|
|
28
|
+
rest = rest.slice(index).trimStart();
|
|
29
|
+
}
|
|
30
|
+
return parts;
|
|
31
|
+
}
|
|
32
|
+
export class TelegramAdapter {
|
|
33
|
+
runtime;
|
|
34
|
+
config;
|
|
35
|
+
#core;
|
|
36
|
+
#pending = new Set();
|
|
37
|
+
#commitQueue = [];
|
|
38
|
+
#token;
|
|
39
|
+
#base;
|
|
40
|
+
#bot;
|
|
41
|
+
#consumerConflict = false;
|
|
42
|
+
#nextOffset;
|
|
43
|
+
constructor(runtime, config, env = process.env, baseUrl = 'https://api.telegram.org', jobs) {
|
|
44
|
+
this.runtime = runtime;
|
|
45
|
+
this.config = config;
|
|
46
|
+
const token = env[config.token_env];
|
|
47
|
+
if (!token || !/^\d+:[A-Za-z0-9_-]{20,}$/.test(token))
|
|
48
|
+
throw new Error('Telegram token is missing or malformed');
|
|
49
|
+
if (config.allowed_user_ids.length === 0)
|
|
50
|
+
throw new Error('Telegram requires at least one allowed user');
|
|
51
|
+
this.#token = token;
|
|
52
|
+
this.#base = baseUrl.replace(/\/$/, '');
|
|
53
|
+
this.#core = new ChannelCore(runtime, env, jobs);
|
|
54
|
+
}
|
|
55
|
+
async #call(method, body, signal) {
|
|
56
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
57
|
+
const response = await fetch(`${this.#base}/bot${this.#token}/${method}`, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
...(body instanceof FormData ? {} : { headers: { 'Content-Type': 'application/json' } }),
|
|
60
|
+
body: body instanceof FormData ? body : JSON.stringify(body),
|
|
61
|
+
...(signal ? { signal } : {}),
|
|
62
|
+
redirect: 'error'
|
|
63
|
+
});
|
|
64
|
+
const parsed = Envelope.parse(await response.json());
|
|
65
|
+
if (response.status === 409 || parsed.error_code === 409) {
|
|
66
|
+
this.#consumerConflict = true;
|
|
67
|
+
throw new TelegramConflictError(`Telegram consumer conflict: ${parsed.description ?? 'another poller or webhook owns the bot'}`);
|
|
68
|
+
}
|
|
69
|
+
if (response.status === 429 && parsed.parameters?.retry_after && attempt < 3) {
|
|
70
|
+
// The abortable timer removes its listener, so a long-lived signal does not accumulate them.
|
|
71
|
+
await sleep(Math.min(parsed.parameters?.retry_after ?? 1, 60) * 1000, undefined, signal ? { signal } : {})
|
|
72
|
+
.catch(() => { throw new Error('Telegram retry aborted'); });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (!response.ok || !parsed.ok) {
|
|
76
|
+
const message = `Telegram ${method} failed: ${parsed.description ?? response.status}`;
|
|
77
|
+
if (response.status >= 400 && response.status < 500)
|
|
78
|
+
throw new DeliveryRejectedError(message);
|
|
79
|
+
throw new Error(message);
|
|
80
|
+
}
|
|
81
|
+
return parsed.result;
|
|
82
|
+
}
|
|
83
|
+
throw new Error(`Telegram ${method} retry limit reached`);
|
|
84
|
+
}
|
|
85
|
+
async initialize(signal) {
|
|
86
|
+
const me = User.parse(await this.#call('getMe', {}, signal));
|
|
87
|
+
if (!me.is_bot)
|
|
88
|
+
throw new Error('Telegram token does not identify a bot');
|
|
89
|
+
const webhook = z.object({ url: z.string() }).passthrough().parse(await this.#call('getWebhookInfo', {}, signal));
|
|
90
|
+
if (webhook.url)
|
|
91
|
+
throw new TelegramConflictError('Telegram webhook is configured; polling cannot start until explicit handover');
|
|
92
|
+
this.#bot = me;
|
|
93
|
+
return { botId: me.id, username: me.username ?? null };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Send one notice to an allowlisted private chat (for plugins and jobs).
|
|
97
|
+
* With `parseMode`, Telegram returns rendered text, so the receipt binds the
|
|
98
|
+
* submitted digest and provider message ID instead of the echoed text.
|
|
99
|
+
*/
|
|
100
|
+
async sendNotice(chatId, body, signal, parseMode) {
|
|
101
|
+
if (this.runtime.store.admissionFence())
|
|
102
|
+
throw new Error('Profile admission is fenced for rollback');
|
|
103
|
+
if (!Number.isSafeInteger(chatId) || chatId <= 0 || !this.config.allowed_user_ids.includes(chatId))
|
|
104
|
+
throw new Error('Notice chat is outside the Telegram allowlist');
|
|
105
|
+
if (!body.trim() || Buffer.byteLength(body, 'utf16le') / 2 > 3900)
|
|
106
|
+
throw new Error('Notice exceeds one Telegram message');
|
|
107
|
+
const message = Message.parse(await this.#call('sendMessage', { chat_id: chatId, text: body,
|
|
108
|
+
...(parseMode ? { parse_mode: parseMode } : {}), disable_web_page_preview: true }, signal));
|
|
109
|
+
if (message.chat.id !== chatId || message.chat.type !== 'private' || message.message_id <= 0 ||
|
|
110
|
+
(!parseMode && message.text !== body))
|
|
111
|
+
throw new Error('Telegram notice receipt differs from bound recipient or submitted body');
|
|
112
|
+
return { message_id: String(message.message_id), chat_id: chatId, chat_type: 'private',
|
|
113
|
+
body_sha256: createHash('sha256').update(body).digest('hex') };
|
|
114
|
+
}
|
|
115
|
+
/** `signal` stops intake; `deliverySignal` aborts replies already admitted from this poll. */
|
|
116
|
+
async pollOnce(signal, deliverySignal = signal) {
|
|
117
|
+
if (this.runtime.store.admissionFence())
|
|
118
|
+
throw new Error('Profile admission is fenced for rollback');
|
|
119
|
+
if (this.#consumerConflict)
|
|
120
|
+
throw new TelegramConflictError('Telegram polling is disabled after a consumer conflict');
|
|
121
|
+
if (!this.#bot)
|
|
122
|
+
await this.initialize(signal);
|
|
123
|
+
const account = String(this.#bot?.id);
|
|
124
|
+
const cursorKey = `telegram:${account}:offset`;
|
|
125
|
+
this.#nextOffset ??= Number(this.runtime.store.cursor(cursorKey) ?? '0');
|
|
126
|
+
const offset = this.#nextOffset;
|
|
127
|
+
const raw = await this.#call('getUpdates', { offset, timeout: 25, limit: 50, allowed_updates: ['message'] }, signal);
|
|
128
|
+
const updates = z.array(Update).max(50).parse(raw).filter(update => update.update_id >= offset).sort((a, b) => a.update_id - b.update_id);
|
|
129
|
+
for (const update of updates) {
|
|
130
|
+
const next = update.update_id + 1;
|
|
131
|
+
const entry = { next, accepted: false, done: false };
|
|
132
|
+
this.#commitQueue.push(entry);
|
|
133
|
+
let readyResolve = () => { };
|
|
134
|
+
let readyReject = () => { };
|
|
135
|
+
const ready = new Promise((resolve, reject) => { readyResolve = () => { entry.accepted = true; resolve(); }; readyReject = reject; });
|
|
136
|
+
const operation = this.#handleUpdate(update, readyResolve, deliverySignal).catch(error => {
|
|
137
|
+
readyReject(error);
|
|
138
|
+
this.runtime.store.setCursor(`telegram:${account}:last_error`, error instanceof Error ? error.message.slice(0, 500) : 'unknown');
|
|
139
|
+
}).finally(() => {
|
|
140
|
+
if (!entry.accepted) {
|
|
141
|
+
const index = this.#commitQueue.indexOf(entry);
|
|
142
|
+
if (index >= 0)
|
|
143
|
+
this.#commitQueue.splice(index, 1);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
entry.done = true;
|
|
147
|
+
while (this.#commitQueue[0]?.done) {
|
|
148
|
+
const completed = this.#commitQueue.shift();
|
|
149
|
+
if (completed)
|
|
150
|
+
this.runtime.store.setCursor(cursorKey, String(completed.next));
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
this.#pending.add(operation);
|
|
154
|
+
void operation.finally(() => this.#pending.delete(operation));
|
|
155
|
+
await ready;
|
|
156
|
+
this.#nextOffset = next;
|
|
157
|
+
}
|
|
158
|
+
return updates.length;
|
|
159
|
+
}
|
|
160
|
+
async drain() { await Promise.allSettled([...this.#pending]); }
|
|
161
|
+
/**
|
|
162
|
+
* Polls until `signal` aborts, then waits for admitted updates. Their replies
|
|
163
|
+
* use `deliverySignal`, so a shutdown can stop intake before cutting delivery.
|
|
164
|
+
*/
|
|
165
|
+
async run(signal, deliverySignal = signal) {
|
|
166
|
+
await this.initialize(signal);
|
|
167
|
+
let failures = 0;
|
|
168
|
+
while (!signal.aborted) {
|
|
169
|
+
try {
|
|
170
|
+
await this.pollOnce(signal, deliverySignal);
|
|
171
|
+
failures = 0;
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
if (signal.aborted)
|
|
175
|
+
break;
|
|
176
|
+
if (error instanceof TelegramConflictError)
|
|
177
|
+
throw error;
|
|
178
|
+
failures++;
|
|
179
|
+
// Abort-aware so shutdown does not wait out the reconnect backoff.
|
|
180
|
+
await sleep(Math.min(30_000, 1000 * 2 ** Math.min(failures, 5)), undefined, { signal }).catch(() => { });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
await this.drain();
|
|
184
|
+
}
|
|
185
|
+
async #handleUpdate(update, ready, signal) {
|
|
186
|
+
const message = update.message;
|
|
187
|
+
if (!message?.from || message.from.is_bot || message.from.id === this.#bot?.id) {
|
|
188
|
+
ready();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (!this.config.allowed_user_ids.includes(message.from.id)) {
|
|
192
|
+
ready();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (message.chat.type !== 'private' && !this.config.allowed_chat_ids.includes(message.chat.id)) {
|
|
196
|
+
ready();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (!['private', 'group', 'supergroup'].includes(message.chat.type)) {
|
|
200
|
+
ready();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const text = message.text ?? message.caption ?? '';
|
|
204
|
+
if (message.chat.type !== 'private') {
|
|
205
|
+
const username = this.#bot?.username;
|
|
206
|
+
if (!username) {
|
|
207
|
+
ready();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (!new RegExp(`(^|\\s)@${username}(?=\\s|$)`, 'i').test(text) && !new RegExp(`^/\\w+@${username}(?=\\s|$)`, 'i').test(text)) {
|
|
211
|
+
ready();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const cleanText = this.#bot?.username ? text.replace(new RegExp(`(^|\\s)@${this.#bot.username}(?=\\s|$)`, 'gi'), '$1').replace(new RegExp(`^(/\\w+)@${this.#bot.username}(?=\\s|$)`, 'i'), '$1').trim() : text;
|
|
216
|
+
const attachments = [];
|
|
217
|
+
if (message.document) {
|
|
218
|
+
const doc = message.document;
|
|
219
|
+
const attachment = { name: doc.file_name.slice(0, 128), media_type: doc.mime_type.slice(0, 100), external_id: doc.file_id.slice(0, 256) };
|
|
220
|
+
if ((doc.file_size ?? Number.POSITIVE_INFINITY) <= 2 * 1024 * 1024 && ['text/plain', 'text/markdown', 'application/json'].includes(doc.mime_type)) {
|
|
221
|
+
const file = z.object({ file_path: z.string() }).passthrough().parse(await this.#call('getFile', { file_id: doc.file_id }, signal));
|
|
222
|
+
if (/^[A-Za-z0-9_./-]{1,512}$/.test(file.file_path) && !file.file_path.split('/').includes('..')) {
|
|
223
|
+
const response = await fetch(`${this.#base}/file/bot${this.#token}/${file.file_path}`, { ...(signal ? { signal } : {}), redirect: 'error' });
|
|
224
|
+
if (response.ok) {
|
|
225
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
226
|
+
if (bytes.length <= 2 * 1024 * 1024)
|
|
227
|
+
attachment.bytes = bytes;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
attachments.push(attachment);
|
|
232
|
+
}
|
|
233
|
+
const event = { platform: 'telegram',
|
|
234
|
+
channel_type: message.chat.type === 'private' ? 'telegram_private'
|
|
235
|
+
: message.chat.type === 'group' ? 'telegram_group' : 'telegram_supergroup',
|
|
236
|
+
account: String(this.#bot?.id), user: String(message.from.id), conversation: String(message.chat.id), thread: message.message_thread_id === undefined ? null : String(message.message_thread_id), update_id: String(update.update_id), reply_to: String(message.message_id), text: cleanText, attachments, authorized: true, bot_message: false };
|
|
237
|
+
const typingBody = { chat_id: message.chat.id, action: 'typing', ...(message.message_thread_id === undefined ? {} : { message_thread_id: message.message_thread_id }) };
|
|
238
|
+
let typingInFlight = null;
|
|
239
|
+
const showTyping = () => {
|
|
240
|
+
if (!typingInFlight) {
|
|
241
|
+
const requestSignal = signal ? AbortSignal.any([signal, AbortSignal.timeout(2500)]) : AbortSignal.timeout(2500);
|
|
242
|
+
typingInFlight = this.#call('sendChatAction', typingBody, requestSignal).then(() => { }, () => { }).finally(() => { typingInFlight = null; });
|
|
243
|
+
}
|
|
244
|
+
return typingInFlight;
|
|
245
|
+
};
|
|
246
|
+
const initialTyping = showTyping();
|
|
247
|
+
const typing = setInterval(() => { void showTyping(); }, 4000);
|
|
248
|
+
try {
|
|
249
|
+
const operation = this.#core.handle(event, async (reply) => { await initialTyping; return this.#deliver(message, reply, signal); });
|
|
250
|
+
ready();
|
|
251
|
+
await operation;
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
clearInterval(typing);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async #deliver(message, reply, signal) {
|
|
258
|
+
const common = { chat_id: message.chat.id, reply_parameters: { message_id: message.message_id }, ...(message.message_thread_id === undefined ? {} : { message_thread_id: message.message_thread_id }) };
|
|
259
|
+
let last = null;
|
|
260
|
+
try {
|
|
261
|
+
for (const part of splitText(reply.text)) {
|
|
262
|
+
const response = Message.parse(await this.#call('sendMessage', { ...common, text: part }, signal));
|
|
263
|
+
last = String(response.message_id);
|
|
264
|
+
}
|
|
265
|
+
for (const [index, id] of reply.artifactIds.slice(0, 2).entries()) {
|
|
266
|
+
const bytes = await this.runtime.artifacts.get(id);
|
|
267
|
+
const form = new FormData();
|
|
268
|
+
form.set('chat_id', String(message.chat.id));
|
|
269
|
+
form.set('reply_parameters', JSON.stringify({ message_id: message.message_id }));
|
|
270
|
+
if (message.message_thread_id !== undefined)
|
|
271
|
+
form.set('message_thread_id', String(message.message_thread_id));
|
|
272
|
+
form.set('document', new Blob([new Uint8Array(bytes)], { type: 'application/octet-stream' }), index === 0 ? 'research-brief.json' : 'research-brief.md');
|
|
273
|
+
const response = Message.parse(await this.#call('sendDocument', form, signal));
|
|
274
|
+
last = String(response.message_id);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
if (last)
|
|
279
|
+
throw new Error(`Telegram delivery may be partial: ${error instanceof Error ? error.message : 'unknown error'}`);
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
return last;
|
|
283
|
+
}
|
|
284
|
+
}
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export type ToolContext = 'direct' | 'peer' | 'scheduled';
|
|
3
|
+
export type ToolEffect = 'read' | 'artifact-write' | 'memory-write' | 'external-write';
|
|
4
|
+
/** Constructed only by first-party code after proving that an effect did not open. */
|
|
5
|
+
export declare class ProvenNoEffectError extends Error {
|
|
6
|
+
readonly code: string;
|
|
7
|
+
constructor(message: string, code: string);
|
|
8
|
+
}
|
|
9
|
+
export declare const ToolResultSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
10
|
+
ok: z.ZodLiteral<true>;
|
|
11
|
+
value: z.ZodOptional<z.ZodUnknown>;
|
|
12
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
13
|
+
ok: z.ZodLiteral<false>;
|
|
14
|
+
code: z.ZodString;
|
|
15
|
+
message: z.ZodString;
|
|
16
|
+
effect: z.ZodEnum<{
|
|
17
|
+
none: "none";
|
|
18
|
+
unknown: "unknown";
|
|
19
|
+
}>;
|
|
20
|
+
}, z.core.$strict>], "ok">;
|
|
21
|
+
export type ToolResult = z.infer<typeof ToolResultSchema>;
|
|
22
|
+
export interface Tool {
|
|
23
|
+
name: string;
|
|
24
|
+
description: string;
|
|
25
|
+
input: z.ZodType<unknown>;
|
|
26
|
+
output: z.ZodType<unknown>;
|
|
27
|
+
effect: ToolEffect;
|
|
28
|
+
contexts: readonly ToolContext[];
|
|
29
|
+
timeoutMs: number;
|
|
30
|
+
invoke(input: unknown, signal: AbortSignal): Promise<unknown>;
|
|
31
|
+
}
|
|
32
|
+
export declare function defineTool<I extends z.ZodTypeAny, O extends z.ZodTypeAny>(definition: {
|
|
33
|
+
name: string;
|
|
34
|
+
description: string;
|
|
35
|
+
input: I;
|
|
36
|
+
output: O;
|
|
37
|
+
effect: ToolEffect;
|
|
38
|
+
contexts: readonly ToolContext[];
|
|
39
|
+
timeoutMs: number;
|
|
40
|
+
handler: (input: z.infer<I>, signal: AbortSignal) => Promise<z.infer<O>>;
|
|
41
|
+
}): Tool;
|
|
42
|
+
export declare class ToolRegistry {
|
|
43
|
+
#private;
|
|
44
|
+
register(tool: Tool): void;
|
|
45
|
+
list(context: ToolContext): Tool[];
|
|
46
|
+
schemas(context: ToolContext): object[];
|
|
47
|
+
call(name: string, args: unknown, context: ToolContext, signal: AbortSignal): Promise<ToolResult>;
|
|
48
|
+
}
|