@hivemagnus/ardent 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +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
package/dist/a2a.js
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { Role, TaskState } from '@a2a-js/sdk';
|
|
3
|
+
import { AgentEvent } from '@a2a-js/sdk/server';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { TEAM_TASK_METADATA_KEY, TEAM_CLARIFICATION_METADATA_KEY, TeamEnvelope as TeamEnvelopeSchema, TeamResult as TeamResultSchema, validateTeamEnvelope, validateTeamResult } from './team-contract.js';
|
|
6
|
+
import { TeamControlService } from './team-control.js';
|
|
7
|
+
import { TeamChildService } from './team-child.js';
|
|
8
|
+
import { TeamArtifacts } from './team-artifacts.js';
|
|
9
|
+
import { CodexHarnessBackend } from './codex.js';
|
|
10
|
+
import { LlamaCppBackend } from './llamacpp.js';
|
|
11
|
+
import { ToolRegistry } from './tools.js';
|
|
12
|
+
const maxPartBytes = 2 * 1024 * 1024;
|
|
13
|
+
/** An extracted article is an owner read when its connector result binds the
|
|
14
|
+
* exact request URL, source URL, retrieval time, and content digest. It remains
|
|
15
|
+
* unverified reporting, but its URL can support a read-only team check. */
|
|
16
|
+
export function observedOwnerSourceReference(name, value) {
|
|
17
|
+
if (name === 'news_article') {
|
|
18
|
+
const article = z.object({ ok: z.literal(true), url: z.string().url(), source_url: z.string().url(),
|
|
19
|
+
source_id: z.string().regex(/^[a-f0-9]{16}$/), retrieved_at: z.string().datetime({ offset: true }),
|
|
20
|
+
content_sha256: z.string().regex(/^[a-f0-9]{64}$/) }).passthrough().safeParse(value);
|
|
21
|
+
if (!article.success || new URL(article.data.url).protocol !== 'https:' ||
|
|
22
|
+
new URL(article.data.source_url).protocol !== 'https:' ||
|
|
23
|
+
article.data.source_url !== article.data.url ||
|
|
24
|
+
article.data.source_id !== createHash('sha256').update(article.data.url).digest('hex').slice(0, 16))
|
|
25
|
+
return null;
|
|
26
|
+
return `source:${article.data.source_url}`;
|
|
27
|
+
}
|
|
28
|
+
const source = z.string().url().safeParse(value.source);
|
|
29
|
+
return source.success ? `source:${source.data}` : null;
|
|
30
|
+
}
|
|
31
|
+
import { MAGNUS_OWNER_READ_TOOLS } from './magnus-owner-reads.js';
|
|
32
|
+
/** Read-only tools whose results carry an exact owner source URL usable as team evidence. */
|
|
33
|
+
const ownerReadTools = new Set(['magnus_fleet_inventory', 'magnus_fleet_readiness', 'magnus_node_health', 'magnus_node_dependency_impact', 'magnus_anomaly_summary', 'magnus_command_status',
|
|
34
|
+
...MAGNUS_OWNER_READ_TOOLS, 'news_article']);
|
|
35
|
+
export function observedOwnerToolResult(detail) {
|
|
36
|
+
const observed = z.object({ name: z.string(), result: z.object({ ok: z.literal(true),
|
|
37
|
+
value: z.record(z.string(), z.unknown()) }).passthrough() }).safeParse(detail);
|
|
38
|
+
if (!observed.success || !ownerReadTools.has(observed.data.name))
|
|
39
|
+
return null;
|
|
40
|
+
const reference = observedOwnerSourceReference(observed.data.name, observed.data.result.value);
|
|
41
|
+
return { name: observed.data.name, value: observed.data.result.value, reference };
|
|
42
|
+
}
|
|
43
|
+
function textPart(value) {
|
|
44
|
+
return { content: { $case: 'text', value }, metadata: undefined, filename: '', mediaType: 'text/plain' };
|
|
45
|
+
}
|
|
46
|
+
function messageText(message) {
|
|
47
|
+
const pieces = [];
|
|
48
|
+
for (const part of message.parts) {
|
|
49
|
+
const content = part.content;
|
|
50
|
+
if (!content)
|
|
51
|
+
throw new Error('A2A part is missing content');
|
|
52
|
+
if (content.$case === 'text')
|
|
53
|
+
pieces.push(content.value);
|
|
54
|
+
else if (content.$case === 'data')
|
|
55
|
+
pieces.push(`Structured data: ${JSON.stringify(content.value)}`);
|
|
56
|
+
else if (content.$case === 'raw') {
|
|
57
|
+
if (content.value.byteLength > maxPartBytes)
|
|
58
|
+
throw new Error('A2A raw part exceeds 2 MiB');
|
|
59
|
+
if (!['text/plain', 'text/markdown', 'application/json'].includes(part.mediaType))
|
|
60
|
+
throw new Error(`Unsupported A2A raw media type: ${part.mediaType}`);
|
|
61
|
+
pieces.push(`Attached ${part.mediaType} ${part.filename || 'file'}:\n${content.value.toString('utf8')}`);
|
|
62
|
+
}
|
|
63
|
+
else if (content.$case === 'url') {
|
|
64
|
+
const url = new URL(content.value);
|
|
65
|
+
if (url.protocol !== 'https:' || url.username || url.password)
|
|
66
|
+
throw new Error('A2A file URL must be credential-free HTTPS');
|
|
67
|
+
pieces.push(`Attached URL (${part.mediaType}): ${url.href}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const prompt = pieces.join('\n\n').trim();
|
|
71
|
+
if (!prompt || prompt.length > 16000)
|
|
72
|
+
throw new Error('A2A message text must be 1 to 16000 characters');
|
|
73
|
+
return prompt;
|
|
74
|
+
}
|
|
75
|
+
function status(taskId, contextId, state, message) {
|
|
76
|
+
return { taskId, contextId, status: { state, timestamp: new Date().toISOString(), message: message ? { role: Role.ROLE_AGENT, messageId: randomUUID(), parts: [textPart(message)], taskId, contextId, extensions: [], metadata: {}, referenceTaskIds: [] } : undefined }, metadata: undefined };
|
|
77
|
+
}
|
|
78
|
+
export class ArdentA2AExecutor {
|
|
79
|
+
runtime;
|
|
80
|
+
env;
|
|
81
|
+
#operations = new Map();
|
|
82
|
+
#cancelled = new Set();
|
|
83
|
+
#control;
|
|
84
|
+
#children;
|
|
85
|
+
#artifacts;
|
|
86
|
+
constructor(runtime, env = process.env) {
|
|
87
|
+
this.runtime = runtime;
|
|
88
|
+
this.env = env;
|
|
89
|
+
runtime.store.db.exec('CREATE TABLE IF NOT EXISTS a2a_operation_map (a2a_task_id TEXT PRIMARY KEY, operation_id TEXT NOT NULL, tenant TEXT NOT NULL, caller TEXT NOT NULL)');
|
|
90
|
+
runtime.store.db.exec('CREATE TABLE IF NOT EXISTS a2a_team_missions (a2a_task_id TEXT PRIMARY KEY, tenant TEXT NOT NULL, caller TEXT NOT NULL, envelope_json TEXT NOT NULL)');
|
|
91
|
+
this.#control = new TeamControlService(runtime);
|
|
92
|
+
this.#children = new TeamChildService(runtime, env);
|
|
93
|
+
this.#artifacts = new TeamArtifacts(runtime, runtime.config, env);
|
|
94
|
+
// Unacknowledged settlements stay retained for a later flush; never crash startup.
|
|
95
|
+
void this.#children.flush().catch(() => { });
|
|
96
|
+
}
|
|
97
|
+
async execute(request, bus) {
|
|
98
|
+
const caller = request.context.user;
|
|
99
|
+
if (!caller?.isAuthenticated || !caller.userName)
|
|
100
|
+
throw new Error('Authenticated A2A caller required');
|
|
101
|
+
const taskId = request.taskId;
|
|
102
|
+
const contextId = request.contextId;
|
|
103
|
+
const snapshot = request.task ?? { id: taskId, contextId, status: { state: TaskState.TASK_STATE_SUBMITTED, timestamp: new Date().toISOString(), message: undefined }, artifacts: [], history: [request.userMessage], metadata: request.userMessage.metadata };
|
|
104
|
+
bus.publish(AgentEvent.task(snapshot));
|
|
105
|
+
const metadata = request.userMessage.metadata ?? {};
|
|
106
|
+
if ('hermesTeamControl' in metadata) {
|
|
107
|
+
try {
|
|
108
|
+
if (Object.keys(metadata).length !== 1 || request.userMessage.parts.length !== 1 ||
|
|
109
|
+
request.userMessage.parts[0]?.content?.$case !== 'text' ||
|
|
110
|
+
request.userMessage.parts[0].content.value !== 'HERMES_COORDINATION_CONTROL')
|
|
111
|
+
throw new Error('Control message mixes task input or metadata');
|
|
112
|
+
const receipt = this.#control.handle(metadata.hermesTeamControl, request.context.tenant ?? '', caller.userName);
|
|
113
|
+
bus.publish(AgentEvent.artifactUpdate({ taskId, contextId, artifact: {
|
|
114
|
+
artifactId: randomUUID(), name: 'Team control receipt', description: 'Authenticated deterministic control response',
|
|
115
|
+
parts: [textPart(JSON.stringify(receipt))], metadata: undefined, extensions: []
|
|
116
|
+
}, append: false, lastChunk: true, metadata: undefined }));
|
|
117
|
+
bus.publish(AgentEvent.statusUpdate(status(taskId, contextId, TaskState.TASK_STATE_COMPLETED)));
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
bus.publish(AgentEvent.statusUpdate(status(taskId, contextId, TaskState.TASK_STATE_REJECTED, error instanceof Error ? error.message : 'Invalid team control')));
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if ((TEAM_CLARIFICATION_METADATA_KEY in metadata && (metadata[TEAM_CLARIFICATION_METADATA_KEY] !== '1.0' || !(TEAM_TASK_METADATA_KEY in metadata))) ||
|
|
125
|
+
('hermesTeamChild' in metadata && !(TEAM_TASK_METADATA_KEY in metadata))) {
|
|
126
|
+
bus.publish(AgentEvent.statusUpdate(status(taskId, contextId, TaskState.TASK_STATE_REJECTED, 'Unsupported team control or extension version')));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
let prompt;
|
|
130
|
+
try {
|
|
131
|
+
prompt = messageText(request.userMessage);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
bus.publish(AgentEvent.statusUpdate(status(taskId, contextId, TaskState.TASK_STATE_FAILED, error instanceof Error ? error.message : 'Unsupported A2A input')));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const rawEnvelope = metadata[TEAM_TASK_METADATA_KEY];
|
|
138
|
+
let envelope;
|
|
139
|
+
let childReference;
|
|
140
|
+
try {
|
|
141
|
+
const tenant = request.context.tenant ?? '';
|
|
142
|
+
const existing = this.runtime.store.db.prepare('SELECT tenant,caller,envelope_json FROM a2a_team_missions WHERE a2a_task_id=?').get(taskId);
|
|
143
|
+
const stored = existing ? z.object({ tenant: z.string(), caller: z.string(), envelope_json: z.string() }).parse(existing) : undefined;
|
|
144
|
+
if (stored && (stored.tenant !== tenant || stored.caller !== caller.userName))
|
|
145
|
+
throw new Error('Team mission owner mismatch');
|
|
146
|
+
if (stored) {
|
|
147
|
+
const admission = this.runtime.store.db.prepare('SELECT admission FROM a2a_team_admission WHERE a2a_task_id=?').get(taskId);
|
|
148
|
+
if (admission && z.object({ admission: z.string() }).parse(admission).admission === 'cancel_requested')
|
|
149
|
+
throw new Error('Cancelled team task cannot continue');
|
|
150
|
+
}
|
|
151
|
+
if (rawEnvelope !== undefined) {
|
|
152
|
+
envelope = validateTeamEnvelope(rawEnvelope, caller.userName, request.userMessage.messageId, prompt, Date.now() / 1000, 'hermesTeamChild' in metadata);
|
|
153
|
+
if ('hermesTeamChild' in metadata)
|
|
154
|
+
childReference = await this.#children.claim(metadata.hermesTeamChild, envelope, taskId, contextId, caller.userName, JSON.stringify(request.userMessage));
|
|
155
|
+
if (stored && stored.envelope_json !== JSON.stringify(envelope))
|
|
156
|
+
throw new Error('Team mission changed during continuation');
|
|
157
|
+
if (!stored)
|
|
158
|
+
this.runtime.store.db.prepare('INSERT INTO a2a_team_missions(a2a_task_id,tenant,caller,envelope_json) VALUES (?,?,?,?)').run(taskId, tenant, caller.userName, JSON.stringify(envelope));
|
|
159
|
+
}
|
|
160
|
+
else if (stored) {
|
|
161
|
+
if (request.task?.status?.state !== TaskState.TASK_STATE_INPUT_REQUIRED)
|
|
162
|
+
throw new Error('Team continuation requires an input-required task');
|
|
163
|
+
const parsed = JSON.parse(stored.envelope_json);
|
|
164
|
+
envelope = TeamEnvelopeSchema.parse(parsed);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
bus.publish(AgentEvent.statusUpdate(status(taskId, contextId, TaskState.TASK_STATE_FAILED, 'Invalid authenticated team envelope')));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (this.#cancelled.has(taskId)) {
|
|
172
|
+
bus.publish(AgentEvent.statusUpdate(status(taskId, contextId, TaskState.TASK_STATE_CANCELED)));
|
|
173
|
+
this.#cancelled.delete(taskId);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
bus.publish(AgentEvent.statusUpdate(status(taskId, contextId, TaskState.TASK_STATE_WORKING)));
|
|
177
|
+
const owner = `a2a:${request.context.tenant ?? ''}:${caller.userName}`;
|
|
178
|
+
const session = this.runtime.store.session(owner, 'peer', contextId);
|
|
179
|
+
const deadline = childReference ? this.#children.deadline(childReference) : envelope?.mission.deadline;
|
|
180
|
+
const existedBefore = Boolean(request.userMessage.messageId && this.runtime.store.db.prepare("SELECT 1 FROM tasks WHERE caller=? AND context='peer' AND idempotency_key=?").get(owner, request.userMessage.messageId));
|
|
181
|
+
const operation = this.runtime.accept(prompt, owner, 'peer', request.userMessage.messageId, session.id, deadline ? deadline * 1000 : undefined);
|
|
182
|
+
if (envelope && operation.state === 'queued')
|
|
183
|
+
this.runtime.store.append(operation.id, 'team_envelope', envelope);
|
|
184
|
+
try {
|
|
185
|
+
this.runtime.store.db.transaction(() => {
|
|
186
|
+
this.runtime.store.db.prepare(`INSERT INTO a2a_operation_map(a2a_task_id,operation_id,tenant,caller) VALUES (?,?,?,?)
|
|
187
|
+
ON CONFLICT(a2a_task_id) DO UPDATE SET operation_id=excluded.operation_id WHERE tenant=excluded.tenant AND caller=excluded.caller`)
|
|
188
|
+
.run(taskId, operation.id, request.context.tenant ?? '', caller.userName);
|
|
189
|
+
if (envelope) {
|
|
190
|
+
this.runtime.store.db.prepare('INSERT OR IGNORE INTO a2a_operation_links(a2a_task_id,operation_id) VALUES (?,?)').run(taskId, operation.id);
|
|
191
|
+
this.runtime.store.db.prepare(`INSERT INTO a2a_team_admission(a2a_task_id,operation_id,version,admission,updated_ms) VALUES (?,?,0,'open',?)
|
|
192
|
+
ON CONFLICT(a2a_task_id) DO UPDATE SET operation_id=excluded.operation_id`).run(taskId, operation.id, Date.now());
|
|
193
|
+
}
|
|
194
|
+
})();
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
// The operation accepted by this call has not been run: a queued task
|
|
198
|
+
// cannot have started an effect. Settle it instead of leaving it queued.
|
|
199
|
+
// A pre-existing operation for the same message keeps its own owner path.
|
|
200
|
+
try {
|
|
201
|
+
if (!existedBefore && this.runtime.store.get(operation.id)?.state === 'queued')
|
|
202
|
+
this.runtime.store.setState(operation.id, 'failed', null, `A2A transport binding failed before dispatch; no effect was started: ${error instanceof Error ? error.message.slice(0, 200) : 'unknown'}`);
|
|
203
|
+
}
|
|
204
|
+
catch { /* Preserve the original binding error. */ }
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
if (childReference)
|
|
208
|
+
this.#children.attach(childReference, operation.id);
|
|
209
|
+
this.#operations.set(taskId, operation.id);
|
|
210
|
+
if (this.#cancelled.has(taskId)) {
|
|
211
|
+
this.runtime.cancel(operation.id);
|
|
212
|
+
bus.publish(AgentEvent.statusUpdate(status(taskId, contextId, TaskState.TASK_STATE_CANCELED)));
|
|
213
|
+
this.#operations.delete(taskId);
|
|
214
|
+
this.#cancelled.delete(taskId);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
const result = await this.runtime.run(operation.id, this.env);
|
|
219
|
+
if (this.#cancelled.has(taskId)) {
|
|
220
|
+
bus.publish(AgentEvent.statusUpdate(status(taskId, contextId, TaskState.TASK_STATE_FAILED, 'Cancellation was requested after execution; inspect operation state before retrying')));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const teamResult = envelope ? await this.#teamResult(envelope, result, operation.id) : undefined;
|
|
224
|
+
if (this.#cancelled.has(taskId)) {
|
|
225
|
+
bus.publish(AgentEvent.statusUpdate(status(taskId, contextId, TaskState.TASK_STATE_FAILED, 'Cancellation was requested during result validation')));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const answerText = teamResult ? JSON.stringify(teamResult) : result.answer;
|
|
229
|
+
if (childReference && teamResult) {
|
|
230
|
+
try {
|
|
231
|
+
await this.#children.settle(childReference, teamResult.outcome, answerText);
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
this.runtime.store.append(operation.id, 'team_child_settlement_pending', { reason: error instanceof Error ? error.message : 'Root settlement unavailable' });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const answer = { artifactId: randomUUID(), name: envelope ? 'Team result' : 'Research answer', description: envelope ? 'Validated team result contract' : 'Ardent answer', parts: [textPart(answerText)], metadata: undefined, extensions: [] };
|
|
238
|
+
bus.publish(AgentEvent.artifactUpdate({ taskId, contextId, artifact: answer, append: false, lastChunk: true, metadata: undefined }));
|
|
239
|
+
if (result.brief) {
|
|
240
|
+
const packet = { artifactId: randomUUID(), name: 'Evidence packet', description: 'Structured source and claim references', parts: [{ content: { $case: 'data', value: result.brief }, metadata: undefined, filename: 'research-brief.json', mediaType: 'application/json' }], metadata: undefined, extensions: [] };
|
|
241
|
+
bus.publish(AgentEvent.artifactUpdate({ taskId, contextId, artifact: packet, append: false, lastChunk: true, metadata: undefined }));
|
|
242
|
+
}
|
|
243
|
+
if (envelope)
|
|
244
|
+
for (const exported of await this.#artifacts.registerResearch(taskId)) {
|
|
245
|
+
const transfer = { artifactId: randomUUID(), name: 'Team artifact export',
|
|
246
|
+
description: 'Task-bound peer export; fetch and recipient delivery require separate receipts',
|
|
247
|
+
parts: [textPart(JSON.stringify(exported))], metadata: undefined, extensions: [] };
|
|
248
|
+
bus.publish(AgentEvent.artifactUpdate({ taskId, contextId, artifact: transfer, append: false,
|
|
249
|
+
lastChunk: true, metadata: undefined }));
|
|
250
|
+
}
|
|
251
|
+
bus.publish(AgentEvent.statusUpdate(status(taskId, contextId, teamResult?.outcome === 'input_required' ? TaskState.TASK_STATE_INPUT_REQUIRED : TaskState.TASK_STATE_COMPLETED)));
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
const message = error instanceof Error ? error.message : 'Ardent execution failed';
|
|
255
|
+
const cancelled = this.#cancelled.has(taskId);
|
|
256
|
+
const operationId = this.#operations.get(taskId);
|
|
257
|
+
const operationState = operationId ? this.runtime.store.get(operationId)?.state : undefined;
|
|
258
|
+
if (childReference && operationId) {
|
|
259
|
+
try {
|
|
260
|
+
await this.#children.settle(childReference, cancelled && operationState === 'unknown' ? 'unknown' : 'failed', message);
|
|
261
|
+
}
|
|
262
|
+
catch (settlementError) {
|
|
263
|
+
this.runtime.store.append(operationId, 'team_child_settlement_pending', { reason: settlementError instanceof Error ? settlementError.message : 'Root settlement unavailable' });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
bus.publish(AgentEvent.statusUpdate(status(taskId, contextId, cancelled && operationState === 'cancelled' ? TaskState.TASK_STATE_CANCELED : TaskState.TASK_STATE_FAILED, cancelled && operationState === 'unknown' ? `Cancellation outcome is unknown: ${message}` : message)));
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
this.#operations.delete(taskId);
|
|
270
|
+
this.#cancelled.delete(taskId);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
async cancelTask(taskId, bus) {
|
|
274
|
+
this.#cancelled.add(taskId);
|
|
275
|
+
const task = this.runtime.store.db.prepare('SELECT context_id FROM a2a_tasks WHERE id=?').get(taskId);
|
|
276
|
+
const contextId = task && typeof task === 'object' && 'context_id' in task && typeof task.context_id === 'string' ? task.context_id : '';
|
|
277
|
+
const operation = this.#operations.get(taskId);
|
|
278
|
+
if (!operation) {
|
|
279
|
+
bus.publish(AgentEvent.statusUpdate(status(taskId, contextId, TaskState.TASK_STATE_CANCELED)));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
this.runtime.cancel(operation);
|
|
283
|
+
// The reference SDK requires the task to have actually reached CANCELED
|
|
284
|
+
// when cancelTask returns. Wait for the owned operation to settle first.
|
|
285
|
+
let timer;
|
|
286
|
+
try {
|
|
287
|
+
await Promise.race([
|
|
288
|
+
this.runtime.run(operation, this.env).then(() => { }, () => { }),
|
|
289
|
+
new Promise(resolve => { timer = setTimeout(resolve, 5_000); })
|
|
290
|
+
]);
|
|
291
|
+
}
|
|
292
|
+
finally {
|
|
293
|
+
if (timer)
|
|
294
|
+
clearTimeout(timer);
|
|
295
|
+
}
|
|
296
|
+
if (this.runtime.store.get(operation)?.state === 'cancelled') {
|
|
297
|
+
bus.publish(AgentEvent.statusUpdate(status(taskId, contextId, TaskState.TASK_STATE_CANCELED)));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async #teamResult(envelope, result, operationId) {
|
|
301
|
+
const events = this.runtime.store.events(operationId, 0, 500);
|
|
302
|
+
const canCreateArtifact = envelope.mission.allowed_actions.includes('create_artifact');
|
|
303
|
+
const artifacts = events.filter(event => canCreateArtifact && event.kind === 'research_artifacts').flatMap(event => {
|
|
304
|
+
const detail = z.object({ report: z.object({ id: z.string().regex(/^[a-f0-9]{64}$/) }), markdown: z.object({ id: z.string().regex(/^[a-f0-9]{64}$/) }) }).safeParse(event.detail);
|
|
305
|
+
if (!detail.success)
|
|
306
|
+
return [];
|
|
307
|
+
return [
|
|
308
|
+
{ name: 'research-brief.json', reference: `ardent-artifact:${detail.data.report.id}`, sha256: detail.data.report.id },
|
|
309
|
+
{ name: 'research-brief.md', reference: `ardent-artifact:${detail.data.markdown.id}`, sha256: detail.data.markdown.id }
|
|
310
|
+
];
|
|
311
|
+
});
|
|
312
|
+
const knownReferences = new Set(artifacts.map(item => item.reference));
|
|
313
|
+
for (const event of events.filter(item => item.kind === 'evidence_register')) {
|
|
314
|
+
const register = z.object({ source_ids: z.array(z.string()), artifact_ids: z.array(z.string()) }).safeParse(event.detail);
|
|
315
|
+
if (!register.success)
|
|
316
|
+
continue;
|
|
317
|
+
for (const sourceId of register.data.source_ids)
|
|
318
|
+
knownReferences.add(`source:${sourceId}`);
|
|
319
|
+
if (canCreateArtifact)
|
|
320
|
+
for (const artifactId of register.data.artifact_ids)
|
|
321
|
+
knownReferences.add(`ardent-artifact:${artifactId}`);
|
|
322
|
+
}
|
|
323
|
+
// Typed owner reads return an exact source URL, but are not news research and
|
|
324
|
+
// therefore do not create a research brief or evidence_register event.
|
|
325
|
+
const observedOwnerSources = new Set();
|
|
326
|
+
for (const event of events.filter(item => item.kind === 'tool_result')) {
|
|
327
|
+
const observed = observedOwnerToolResult(event.detail);
|
|
328
|
+
if (!observed)
|
|
329
|
+
continue;
|
|
330
|
+
const { reference } = observed;
|
|
331
|
+
if (reference) {
|
|
332
|
+
knownReferences.add(reference);
|
|
333
|
+
observedOwnerSources.add(reference);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
let submitted = result.brief?.team_result;
|
|
337
|
+
if (!result.brief?.team_result) {
|
|
338
|
+
try {
|
|
339
|
+
const parsed = z.object({ team_result: z.unknown().optional() }).passthrough().safeParse(JSON.parse(result.answer));
|
|
340
|
+
if (parsed.success)
|
|
341
|
+
submitted = parsed.data.team_result;
|
|
342
|
+
}
|
|
343
|
+
catch { /* A non-JSON answer cannot assert mission completion. */ }
|
|
344
|
+
}
|
|
345
|
+
if (submitted !== undefined) {
|
|
346
|
+
try {
|
|
347
|
+
return this.#validateSubmittedTeamResult(submitted, artifacts, envelope, knownReferences);
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
const reason = error instanceof Error ? error.message : 'Invalid team result';
|
|
351
|
+
this.runtime.store.append(operationId, 'team_result_rejected', { reason: reason.slice(0, 1000) });
|
|
352
|
+
if (envelope.mission.allowed_actions.every(action => action === 'observe' || action === 'analyze')) {
|
|
353
|
+
const repaired = await this.#repairTeamResult(submitted, reason, envelope, result, operationId, artifacts, knownReferences, observedOwnerSources);
|
|
354
|
+
if (repaired)
|
|
355
|
+
return repaired;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const candidate = {
|
|
360
|
+
version: '1.0', mission_id: envelope.mission.id, mission_revision: envelope.mission.revision, task_id: envelope.task_id,
|
|
361
|
+
outcome: 'partial', summary: result.answer.slice(0, 4000) || 'Research completed without a validated team conclusion',
|
|
362
|
+
checks: envelope.mission.acceptance_criteria.map(criterion => ({ criterion, status: 'unknown', evidence: [] })),
|
|
363
|
+
artifacts, uncertain_effects: []
|
|
364
|
+
};
|
|
365
|
+
return validateTeamResult(JSON.stringify(candidate), envelope);
|
|
366
|
+
}
|
|
367
|
+
#validateSubmittedTeamResult(submitted, artifacts, envelope, knownReferences) {
|
|
368
|
+
const parsed = TeamResultSchema.parse(submitted);
|
|
369
|
+
if (!envelope.mission.allowed_actions.includes('create_artifact') && parsed.artifacts.length)
|
|
370
|
+
throw new Error('Team result declares an artifact without create_artifact authority');
|
|
371
|
+
for (const item of parsed.artifacts)
|
|
372
|
+
if (!knownReferences.has(item.reference) || item.reference !== `ardent-artifact:${item.sha256}`)
|
|
373
|
+
throw new Error('Team result declares an unobserved artifact');
|
|
374
|
+
return validateTeamResult(JSON.stringify({ ...parsed,
|
|
375
|
+
artifacts: [...artifacts, ...parsed.artifacts.filter(item => !artifacts.some(real => real.name === item.name))] }), envelope, knownReferences);
|
|
376
|
+
}
|
|
377
|
+
async #repairTeamResult(submitted, reason, envelope, result, operationId, artifacts, knownReferences, observedOwnerSources) {
|
|
378
|
+
const shape = z.object({ version: z.unknown().optional(), mission_id: z.string(), mission_revision: z.number(), task_id: z.string(),
|
|
379
|
+
outcome: z.string(), summary: z.string(), checks: z.array(z.object({ criterion: z.string(), status: z.string(),
|
|
380
|
+
evidence: z.union([z.array(z.string()), z.string()]) }).strict()), artifacts: z.array(z.unknown()), uncertain_effects: z.array(z.string()) }).strict();
|
|
381
|
+
const original = shape.safeParse(submitted);
|
|
382
|
+
const references = [...knownReferences].sort();
|
|
383
|
+
const soleOwnerSource = observedOwnerSources.size === 1 ? [...observedOwnerSources][0] : undefined;
|
|
384
|
+
if (!original.success || original.data.version !== '1.0' || original.data.mission_id !== envelope.mission.id || original.data.mission_revision !== envelope.mission.revision
|
|
385
|
+
|| original.data.task_id !== envelope.task_id || !['completed', 'complete', 'partial', 'input_required'].includes(original.data.outcome)
|
|
386
|
+
|| references.length > 64 || references.some(reference => reference.length > 512)
|
|
387
|
+
|| Buffer.byteLength(JSON.stringify(submitted)) > 16_000 || result.task.deadline_ms - Date.now() < 5_000
|
|
388
|
+
|| this.runtime.store.admissionFence())
|
|
389
|
+
return null;
|
|
390
|
+
const admission = this.runtime.store.db.prepare('SELECT admission FROM a2a_team_admission WHERE operation_id=? LIMIT 1').get(operationId);
|
|
391
|
+
if (!z.object({ admission: z.literal('open') }).safeParse(admission).success)
|
|
392
|
+
return null;
|
|
393
|
+
const singleReadCheck = original.data.checks.length === 1 && envelope.mission.acceptance_criteria.length === 1
|
|
394
|
+
&& envelope.mission.expected_artifacts.length === 0 && original.data.artifacts.length === 0
|
|
395
|
+
&& original.data.checks[0]?.criterion === envelope.mission.acceptance_criteria[0]
|
|
396
|
+
&& ['pass', 'passed'].includes(original.data.checks[0]?.status ?? '');
|
|
397
|
+
const originalEvidence = original.data.checks[0]?.evidence;
|
|
398
|
+
const executionCitation = singleReadCheck && Array.isArray(originalEvidence) && originalEvidence.length === 1
|
|
399
|
+
&& /^source:exec-[A-Za-z0-9._:-]{1,128}$/.test(originalEvidence[0] ?? '') ? originalEvidence[0] : undefined;
|
|
400
|
+
const citationCorrection = executionCitation && soleOwnerSource ? { from: executionCitation, to: soleOwnerSource } : undefined;
|
|
401
|
+
const evidenceArrayCorrection = singleReadCheck && typeof originalEvidence === 'string' && knownReferences.has(originalEvidence)
|
|
402
|
+
? { from: originalEvidence, to: [originalEvidence] } : undefined;
|
|
403
|
+
// These are format-only aliases for one observed, read-only owner check.
|
|
404
|
+
// The normal validator still requires an exact mission binding, a known
|
|
405
|
+
// source reference, all checks, and zero uncertain effects for completion.
|
|
406
|
+
if (singleReadCheck && (original.data.outcome === 'complete' || original.data.checks[0]?.status === 'passed'
|
|
407
|
+
|| citationCorrection || evidenceArrayCorrection)) {
|
|
408
|
+
const check = original.data.checks[0];
|
|
409
|
+
const normalized = { ...original.data, outcome: original.data.outcome === 'complete' ? 'completed' : original.data.outcome,
|
|
410
|
+
checks: [{ ...check, status: check.status === 'passed' ? 'pass' : check.status,
|
|
411
|
+
evidence: citationCorrection ? [citationCorrection.to] : evidenceArrayCorrection?.to ?? check.evidence }] };
|
|
412
|
+
try {
|
|
413
|
+
const validated = this.#validateSubmittedTeamResult(normalized, artifacts, envelope, knownReferences);
|
|
414
|
+
this.runtime.store.append(operationId, 'team_result_repair_accepted', { outcome: validated.outcome, tool_calls: 0,
|
|
415
|
+
method: 'observed_read_format' });
|
|
416
|
+
return validated;
|
|
417
|
+
}
|
|
418
|
+
catch { /* The bounded model formatter still must pass the same strict validator. */ }
|
|
419
|
+
}
|
|
420
|
+
const instruction = `Correct only the JSON format of this authenticated read-only team_result. Do not call tools, fetch data, change facts, or add evidence. Return one JSON object containing only team_result. Exact schema: {team_result:{version:"1.0",mission_id:string,mission_revision:integer,task_id:string,outcome:"completed"|"partial"|"failed"|"blocked"|"input_required"|"cancelled"|"unknown",summary:string,checks:[{criterion:string,status:"pass"|"fail"|"unknown",evidence:string[]}],artifacts:[{name:string,reference:string,sha256:string}],uncertain_effects:string[]}}. Exact envelope IDs: mission_id=${JSON.stringify(envelope.mission.id)}, mission_revision=${envelope.mission.revision}, task_id=${JSON.stringify(envelope.task_id)}. Allowed observed references: ${JSON.stringify(references)}. Validation error: ${reason.slice(0, 500)}. Preserve summary, each criterion/status, artifacts, and uncertain_effects exactly. Preserve evidence exactly except this sole allowed citation correction for the one criterion: ${JSON.stringify(citationCorrection ?? null)} and this sole allowed string-to-array correction for the one criterion: ${JSON.stringify(evidenceArrayCorrection ?? null)}. You may normalize outcome "complete" to "completed"; do not upgrade any other outcome. Original team_result: ${JSON.stringify(submitted)}`;
|
|
421
|
+
this.runtime.store.append(operationId, 'team_result_repair_attempt', { reason: reason.slice(0, 500), observed_references: references.length });
|
|
422
|
+
try {
|
|
423
|
+
const signal = AbortSignal.timeout(Math.min(30_000, result.task.deadline_ms - Date.now()));
|
|
424
|
+
let answer;
|
|
425
|
+
let toolCalls = 0;
|
|
426
|
+
if (result.task.backend === 'codex') {
|
|
427
|
+
const formatter = new CodexHarnessBackend(this.runtime.config.providers.codex, this.runtime.profileDir, this.env);
|
|
428
|
+
const corrected = await formatter.run(instruction, 'Return JSON only. No tools, shell, browser, network reads, or external effects.', new ToolRegistry(), 'peer', signal, 0);
|
|
429
|
+
answer = corrected.answer;
|
|
430
|
+
toolCalls = corrected.toolCalls;
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
const formatter = new LlamaCppBackend(this.runtime.config.providers.llamacpp, this.env);
|
|
434
|
+
const corrected = await formatter.complete([{ role: 'system', content: 'Return JSON only. No tools or external effects.' },
|
|
435
|
+
{ role: 'user', content: instruction }], [], signal, false);
|
|
436
|
+
answer = corrected.content;
|
|
437
|
+
toolCalls = corrected.toolCalls.length;
|
|
438
|
+
}
|
|
439
|
+
if (toolCalls || Buffer.byteLength(answer) > 16_000)
|
|
440
|
+
throw new Error('Formatting turn exceeded its tool or response boundary');
|
|
441
|
+
const candidate = z.object({ team_result: TeamResultSchema }).strict().parse(JSON.parse(answer)).team_result;
|
|
442
|
+
// An execution-id placeholder may become the sole observed owner-read
|
|
443
|
+
// source; that exact source string may also be wrapped in one array.
|
|
444
|
+
const prior = original.data;
|
|
445
|
+
const allowedChecks = citationCorrection ? [{ ...prior.checks[0], evidence: [citationCorrection.to] }]
|
|
446
|
+
: evidenceArrayCorrection ? [{ ...prior.checks[0], evidence: evidenceArrayCorrection.to }] : prior.checks;
|
|
447
|
+
if (candidate.mission_id !== prior.mission_id || candidate.mission_revision !== prior.mission_revision
|
|
448
|
+
|| candidate.task_id !== prior.task_id || candidate.summary !== prior.summary
|
|
449
|
+
|| !(JSON.stringify(candidate.checks) === JSON.stringify(prior.checks)
|
|
450
|
+
|| (citationCorrection || evidenceArrayCorrection) && JSON.stringify(candidate.checks) === JSON.stringify(allowedChecks))
|
|
451
|
+
|| JSON.stringify(candidate.artifacts) !== JSON.stringify(prior.artifacts)
|
|
452
|
+
|| JSON.stringify(candidate.uncertain_effects) !== JSON.stringify(prior.uncertain_effects)
|
|
453
|
+
|| !(candidate.outcome === prior.outcome || prior.outcome === 'complete' && candidate.outcome === 'completed'))
|
|
454
|
+
throw new Error('Formatting turn changed mission facts, checks, evidence, or outcome');
|
|
455
|
+
if (this.runtime.store.admissionFence())
|
|
456
|
+
throw new Error('Profile admission closed during formatting correction');
|
|
457
|
+
const validated = this.#validateSubmittedTeamResult(candidate, artifacts, envelope, knownReferences);
|
|
458
|
+
this.runtime.store.append(operationId, 'team_result_repair_accepted', { outcome: validated.outcome, tool_calls: 0 });
|
|
459
|
+
return validated;
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
this.runtime.store.append(operationId, 'team_result_repair_rejected', { reason: error instanceof Error ? error.message.slice(0, 500) : 'Formatting correction unavailable' });
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { type LoadedConfig } from './config.js';
|
|
3
|
+
import type { ArdentRuntime } from './runtime.js';
|
|
4
|
+
import type { JobEngine } from './job-engine.js';
|
|
5
|
+
export declare function ardentOpenApi(): object;
|
|
6
|
+
export declare function createArdentApi(runtime: ArdentRuntime, loaded: LoadedConfig, env?: NodeJS.ProcessEnv, jobEngine?: JobEngine): Hono;
|