@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,153 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { registerNewsTools } from './news.js';
|
|
4
|
+
import { ToolRegistry } from './tools.js';
|
|
5
|
+
const DiscoverySource = z.object({ id: z.string().regex(/^[a-z][a-z0-9_]{1,40}$/),
|
|
6
|
+
query: z.string().trim().min(3).max(300), category: z.enum(['news', 'general']).default('news'),
|
|
7
|
+
time_range: z.enum(['day', 'week', 'month', 'year', 'all']).default('day'), language: z.string().regex(/^[a-z]{2}(?:-[A-Z]{2})?$/).default('en'),
|
|
8
|
+
limit: z.number().int().min(1).max(12), max_articles: z.number().int().min(1).max(5),
|
|
9
|
+
allow_hosts: z.array(z.string().regex(/^[a-z0-9.-]+\.[a-z]{2,}$/)).max(8).default([]),
|
|
10
|
+
include_title_terms: z.array(z.string().trim().min(2).max(80)).min(1).max(32).optional() }).strict();
|
|
11
|
+
export const BoundedNewsInput = z.object({
|
|
12
|
+
query: z.string().trim().min(3).max(300),
|
|
13
|
+
category: z.enum(['news', 'general']).default('news'),
|
|
14
|
+
time_range: z.enum(['day', 'week', 'month']).default('day'),
|
|
15
|
+
language: z.string().regex(/^[a-z]{2}(?:-[A-Z]{2})?$/).default('en'),
|
|
16
|
+
limit: z.number().int().min(1).max(10).default(8),
|
|
17
|
+
max_articles: z.number().int().min(1).max(5).default(3),
|
|
18
|
+
purpose: z.string().regex(/^[a-z][a-z0-9_]{0,63}$/).default('news_packet'),
|
|
19
|
+
/** Retry an empty packet after transient read-only failures until the final attempt. */
|
|
20
|
+
retry_empty: z.boolean().default(false),
|
|
21
|
+
fallback_searches: z.array(z.object({ query: z.string().trim().min(3).max(300),
|
|
22
|
+
category: z.enum(['news', 'general']) }).strict()).min(1).max(2).optional(),
|
|
23
|
+
discovery_sources: z.array(DiscoverySource).min(1).max(12).optional(),
|
|
24
|
+
/** Hosts treated as first-party publishers in source provenance. */
|
|
25
|
+
publisher_hosts: z.array(z.string().regex(/^[a-z0-9.-]+\.[a-z]{2,}$/)).max(32).default([])
|
|
26
|
+
}).strict().superRefine((value, context) => {
|
|
27
|
+
const discoveryIds = value.discovery_sources?.map(item => item.id) ?? [];
|
|
28
|
+
if (new Set(discoveryIds).size !== discoveryIds.length)
|
|
29
|
+
context.addIssue({ code: z.ZodIssueCode.custom, path: ['discovery_sources'], message: 'Duplicate discovery source' });
|
|
30
|
+
});
|
|
31
|
+
const Article = z.object({ url: z.string().url(), source_url: z.string().url(), source_id: z.string(),
|
|
32
|
+
title: z.string(), published_at: z.string().nullable(), retrieved_at: z.string(),
|
|
33
|
+
content_sha256: z.string().regex(/^[a-f0-9]{64}$/), warning: z.string() }).passthrough();
|
|
34
|
+
const Packet = z.object({ status: z.enum(['ok', 'partial', 'empty']),
|
|
35
|
+
articles: z.array(Article).max(5), failures: z.array(z.object({ url: z.string(), error: z.string() })).max(10),
|
|
36
|
+
coverage: z.object({ discovered: z.number().int(), title_prefiltered: z.number().int().optional(),
|
|
37
|
+
extracted: z.number().int() }),
|
|
38
|
+
search: z.object({ retrieved_at: z.string(), unresponsive_engines: z.array(z.unknown()) }).passthrough(),
|
|
39
|
+
artifact: z.object({ id: z.string().regex(/^[a-f0-9]{64}$/) }).passthrough() }).passthrough();
|
|
40
|
+
const hash = (value) => createHash('sha256').update(value).digest('hex');
|
|
41
|
+
/** Bounded first-party retrieval. A packet is source evidence, never claim verification or delivery. */
|
|
42
|
+
export function boundedNewsHandler(runtime, env) {
|
|
43
|
+
return { input: BoundedNewsInput, effect: 'external', run: async ({ job, input, signal, attempt }) => {
|
|
44
|
+
if (job.delivery.kind !== 'none')
|
|
45
|
+
throw new Error('News source packet cannot deliver directly');
|
|
46
|
+
const request = BoundedNewsInput.parse(input);
|
|
47
|
+
const tools = new ToolRegistry();
|
|
48
|
+
registerNewsTools(tools, runtime.config, runtime.artifacts, env);
|
|
49
|
+
const discoverySources = request.discovery_sources ?? [{ id: 'default', query: request.query,
|
|
50
|
+
category: request.category, time_range: request.time_range, language: request.language,
|
|
51
|
+
limit: request.limit, max_articles: request.max_articles, allow_hosts: [] },
|
|
52
|
+
...(request.fallback_searches ?? []).map((item, index) => ({ id: `fallback_${index + 1}`,
|
|
53
|
+
query: item.query, category: item.category, time_range: request.time_range,
|
|
54
|
+
language: request.language, limit: request.limit, max_articles: request.max_articles, allow_hosts: [] }))];
|
|
55
|
+
const packets = [];
|
|
56
|
+
const discoveryFailures = [];
|
|
57
|
+
for (const source of discoverySources) {
|
|
58
|
+
if (source.id.startsWith('fallback_') && packets.some(item => item.packet.articles.length > 0))
|
|
59
|
+
break;
|
|
60
|
+
const result = await tools.call('news_collect', {
|
|
61
|
+
query: source.query, category: source.category, time_range: source.time_range,
|
|
62
|
+
language: source.language, limit: source.limit, max_articles: source.max_articles,
|
|
63
|
+
...('include_title_terms' in source && source.include_title_terms
|
|
64
|
+
? { include_title_terms: source.include_title_terms } : {})
|
|
65
|
+
}, 'scheduled', signal);
|
|
66
|
+
if (!result.ok) {
|
|
67
|
+
if (result.effect === 'unknown')
|
|
68
|
+
return { state: 'unknown',
|
|
69
|
+
receipt: { effect_status: 'unknown', evidence: { code: result.code,
|
|
70
|
+
message: result.message.slice(0, 200), discovery_source_id: source.id,
|
|
71
|
+
prior_discovery_packet_ids: packets.map(item => item.packet.artifact.id),
|
|
72
|
+
discovery_effect: 'unknown', delivery: 'not_attempted' } } };
|
|
73
|
+
discoveryFailures.push({ source_id: source.id, code: result.code,
|
|
74
|
+
message: result.message.slice(0, 200) });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
packets.push({ source, packet: Packet.parse(result.value) });
|
|
78
|
+
}
|
|
79
|
+
if (!packets.length)
|
|
80
|
+
return { state: 'retry',
|
|
81
|
+
receipt: { effect_status: 'none', evidence: { discovery_failures: discoveryFailures } } };
|
|
82
|
+
const first = packets[0]?.packet ?? null;
|
|
83
|
+
const allowed = (host, domains) => !domains.length ||
|
|
84
|
+
domains.some(domain => host === domain || host.endsWith(`.${domain}`));
|
|
85
|
+
const publisher = (host) => request.publisher_hosts
|
|
86
|
+
.some(domain => host === domain || host.endsWith(`.${domain}`));
|
|
87
|
+
const sources = packets.flatMap(({ source, packet }) => packet.articles.filter(article => allowed(new URL(article.url).hostname, source.allow_hosts) &&
|
|
88
|
+
allowed(new URL(article.source_url).hostname, source.allow_hosts)).map(article => ({
|
|
89
|
+
source_id: article.source_id, discovery_source_id: source.id, url: article.url,
|
|
90
|
+
publisher: new URL(article.source_url).hostname, publisher_url: article.source_url,
|
|
91
|
+
published_at: article.published_at, retrieved_at: article.retrieved_at,
|
|
92
|
+
extraction: 'extracted', content_sha256: article.content_sha256,
|
|
93
|
+
reporting_role: 'unknown',
|
|
94
|
+
provenance: publisher(new URL(article.source_url).hostname)
|
|
95
|
+
? 'publisher-search' : 'secondary-reporting',
|
|
96
|
+
claim_verification: 'unverified', title_sha256: hash(article.title)
|
|
97
|
+
})));
|
|
98
|
+
const failures = packets.flatMap(({ source, packet }) => packet.failures.map(item => ({
|
|
99
|
+
discovery_source_id: source.id, url: item.url, error: item.error.slice(0, 200)
|
|
100
|
+
}))).slice(0, 10);
|
|
101
|
+
const discoveryPackets = packets.map(({ source, packet }) => ({ source_id: source.id,
|
|
102
|
+
artifact_id: packet.artifact.id, status: packet.status,
|
|
103
|
+
filtered_articles: packet.articles.length - packet.articles.filter(article => allowed(new URL(article.url).hostname, source.allow_hosts) &&
|
|
104
|
+
allowed(new URL(article.source_url).hostname, source.allow_hosts)).length,
|
|
105
|
+
coverage: packet.coverage, unresponsive_engines: packet.search.unresponsive_engines.length }));
|
|
106
|
+
const coverage = { discovered: packets.reduce((sum, item) => sum + item.packet.coverage.discovered, 0),
|
|
107
|
+
extracted: packets.reduce((sum, item) => sum + item.packet.coverage.extracted, 0) };
|
|
108
|
+
const unresponsiveEngines = packets.reduce((sum, item) => sum + item.packet.search.unresponsive_engines.length, 0);
|
|
109
|
+
// A transient, definitely read-only outage must not seal an empty morning
|
|
110
|
+
// packet on the first attempt. Unknown tool effects return above and are
|
|
111
|
+
// never retried. The final attempt still retains the honest partial packet.
|
|
112
|
+
if (request.retry_empty && !sources.length &&
|
|
113
|
+
(unresponsiveEngines || failures.length || discoveryFailures.length) && attempt < job.policy.max_attempts) {
|
|
114
|
+
return { state: 'retry', receipt: { effect_status: 'none', evidence: {
|
|
115
|
+
reason: 'No extracted article after transient read-only retrieval failures',
|
|
116
|
+
prior_discovery_packet_ids: packets.map(item => item.packet.artifact.id),
|
|
117
|
+
discovery_failures: discoveryFailures, failures, unresponsive_engines: unresponsiveEngines,
|
|
118
|
+
coverage, delivery: 'not_attempted'
|
|
119
|
+
} } };
|
|
120
|
+
}
|
|
121
|
+
const searchedAt = packets.map(item => item.packet.search.retrieved_at).sort().at(-1) ?? null;
|
|
122
|
+
if (!first)
|
|
123
|
+
throw new Error('News packet is unavailable');
|
|
124
|
+
// Retain every attempted search and its failure detail, including empty
|
|
125
|
+
// discovery. The digest-bound packet must match the combined source receipt.
|
|
126
|
+
const packet = packets.length === 1 ? first : { ...first,
|
|
127
|
+
articles: packets.flatMap(item => item.packet.articles).slice(0, 5),
|
|
128
|
+
discovery_packets: discoveryPackets, discovery_failures: discoveryFailures,
|
|
129
|
+
searches: packets.map(item => ({ source_id: item.source.id, query: item.packet.search.query,
|
|
130
|
+
retrieved_at: item.packet.search.retrieved_at, status: item.packet.search.status,
|
|
131
|
+
unresponsive_engines: item.packet.search.unresponsive_engines })), failures, coverage };
|
|
132
|
+
const artifact = packets.length === 1 ? first.artifact
|
|
133
|
+
: await runtime.artifacts.put('news-source-packet.json', 'application/json', Buffer.from(JSON.stringify(packet)));
|
|
134
|
+
return { state: 'completed', receipt: { effect_status: 'confirmed', evidence: {
|
|
135
|
+
purpose: request.purpose, source_packet_id: artifact.id,
|
|
136
|
+
status: discoveryFailures.length || failures.length || unresponsiveEngines ? 'partial' : sources.length ? 'ok' : 'empty',
|
|
137
|
+
searched_at: searchedAt, sources, failures,
|
|
138
|
+
unresponsive_engines: unresponsiveEngines,
|
|
139
|
+
discovery_packets: discoveryPackets, discovery_failures: discoveryFailures,
|
|
140
|
+
coverage_gaps: sources.length ? [] : ['No retained articles; inspect discovery engine failures and empty search coverage'],
|
|
141
|
+
coverage, delivery: 'not_attempted',
|
|
142
|
+
warning: 'Publisher extraction and search rank do not verify claims or independent reporting.'
|
|
143
|
+
} } };
|
|
144
|
+
} };
|
|
145
|
+
}
|
|
146
|
+
/** Built-in scheduled job handlers available to every profile. */
|
|
147
|
+
export function builtinJobHandlers(runtime, env) {
|
|
148
|
+
return { 'news.collect': boundedNewsHandler(runtime, env) };
|
|
149
|
+
}
|
|
150
|
+
/** Validation-only registry (no runtime) for `ardent jobs definitions validate`. */
|
|
151
|
+
export const BUILTIN_JOB_VALIDATION_REGISTRY = {
|
|
152
|
+
'news.collect': { input: BoundedNewsInput, effect: 'external', run: () => Promise.reject(new Error('Validation-only handler')) }
|
|
153
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { JobDefinition, OwnershipCheck } from './job-engine.js';
|
|
2
|
+
/**
|
|
3
|
+
* Default ownership policy: an enabled job explicitly assigned to Ardent may
|
|
4
|
+
* run. Deployments that migrate schedules from another scheduler can add a
|
|
5
|
+
* stricter check through a plugin (`ArdentPlugin.jobOwnership`).
|
|
6
|
+
*/
|
|
7
|
+
export declare function assertJobOwnership(job: JobDefinition): Promise<void>;
|
|
8
|
+
/** Combine the default policy with optional extra checks; every check must pass. */
|
|
9
|
+
export declare function composeOwnershipChecks(extra?: readonly OwnershipCheck[]): OwnershipCheck;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default ownership policy: an enabled job explicitly assigned to Ardent may
|
|
3
|
+
* run. Deployments that migrate schedules from another scheduler can add a
|
|
4
|
+
* stricter check through a plugin (`ArdentPlugin.jobOwnership`).
|
|
5
|
+
*/
|
|
6
|
+
export async function assertJobOwnership(job) {
|
|
7
|
+
if (!job.enabled || job.owner !== 'ardent')
|
|
8
|
+
throw new Error('Job is not assigned to Ardent');
|
|
9
|
+
}
|
|
10
|
+
/** Combine the default policy with optional extra checks; every check must pass. */
|
|
11
|
+
export function composeOwnershipChecks(extra = []) {
|
|
12
|
+
return async (job, now) => {
|
|
13
|
+
await assertJobOwnership(job);
|
|
14
|
+
for (const check of extra)
|
|
15
|
+
await check(job, now);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export type JsonSchemaTarget = 'openapi-3.0' | 'draft-7';
|
|
3
|
+
/**
|
|
4
|
+
* JSON Schema for a request-side Zod schema: tool parameters, API request
|
|
5
|
+
* bodies, configuration, and model output contracts. Fields with defaults are
|
|
6
|
+
* optional because callers may omit them, and refinements and transforms
|
|
7
|
+
* describe what a caller sends. Nested schemas are inlined (no $ref).
|
|
8
|
+
*/
|
|
9
|
+
export declare function toJsonSchema(schema: z.ZodType, target?: JsonSchemaTarget): Record<string, unknown>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* JSON Schema for a request-side Zod schema: tool parameters, API request
|
|
4
|
+
* bodies, configuration, and model output contracts. Fields with defaults are
|
|
5
|
+
* optional because callers may omit them, and refinements and transforms
|
|
6
|
+
* describe what a caller sends. Nested schemas are inlined (no $ref).
|
|
7
|
+
*/
|
|
8
|
+
export function toJsonSchema(schema, target = 'draft-7') {
|
|
9
|
+
return z.toJSONSchema(schema, {
|
|
10
|
+
target, io: 'input', reused: 'inline', unrepresentable: 'any',
|
|
11
|
+
// Zod 4 emits oneOf for discriminated unions. Keep anyOf (equivalent for
|
|
12
|
+
// exclusive variants) because some function-calling schema dialects reject oneOf.
|
|
13
|
+
override: ({ jsonSchema }) => {
|
|
14
|
+
if (jsonSchema.oneOf) {
|
|
15
|
+
jsonSchema.anyOf = jsonSchema.oneOf;
|
|
16
|
+
delete jsonSchema.oneOf;
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ArdentConfig } from './config.js';
|
|
2
|
+
import type { BackendEventSink } from './backend-events.js';
|
|
3
|
+
export type ChatMessage = {
|
|
4
|
+
role: 'system' | 'user' | 'assistant' | 'tool';
|
|
5
|
+
content: string | null;
|
|
6
|
+
tool_call_id?: string;
|
|
7
|
+
tool_calls?: ModelToolCall[];
|
|
8
|
+
};
|
|
9
|
+
export type ModelToolCall = {
|
|
10
|
+
id: string;
|
|
11
|
+
type: 'function';
|
|
12
|
+
function: {
|
|
13
|
+
name: string;
|
|
14
|
+
arguments: string;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export type ModelReply = {
|
|
18
|
+
content: string;
|
|
19
|
+
reasoning?: string;
|
|
20
|
+
toolCalls: ModelToolCall[];
|
|
21
|
+
usage?: {
|
|
22
|
+
promptTokens?: number;
|
|
23
|
+
completionTokens?: number;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
export type LlamaCapabilities = {
|
|
27
|
+
modelListed: boolean | null;
|
|
28
|
+
contextTokens: number | null;
|
|
29
|
+
supportsTools: boolean | null;
|
|
30
|
+
supportsReasoningEffort: boolean | null;
|
|
31
|
+
sampling: {
|
|
32
|
+
temperature: number | null;
|
|
33
|
+
topP: number | null;
|
|
34
|
+
};
|
|
35
|
+
reasoningFormat: string | null;
|
|
36
|
+
chatFormat: string | null;
|
|
37
|
+
};
|
|
38
|
+
export declare function llamaEndpoint(base: string, operation: 'chat/completions' | 'chat/completions/input_tokens' | 'models'): URL;
|
|
39
|
+
export declare function sseData(stream: ReadableStream<Uint8Array>): AsyncGenerator<string>;
|
|
40
|
+
export declare class NativeLlmBackend {
|
|
41
|
+
#private;
|
|
42
|
+
readonly config: ArdentConfig['providers']['llamacpp'];
|
|
43
|
+
readonly env: NodeJS.ProcessEnv;
|
|
44
|
+
constructor(config: ArdentConfig['providers']['llamacpp'], env?: NodeJS.ProcessEnv);
|
|
45
|
+
discover(): Promise<LlamaCapabilities>;
|
|
46
|
+
countInputTokens(messages: ChatMessage[], tools: object[], signal: AbortSignal): Promise<number | null>;
|
|
47
|
+
complete(messages: ChatMessage[], tools: object[], signal: AbortSignal, stream?: boolean, onText?: (chunk: string) => void, onEvent?: BackendEventSink): Promise<ModelReply>;
|
|
48
|
+
}
|
|
49
|
+
export { NativeLlmBackend as LlamaCppBackend };
|
package/dist/llamacpp.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
3
|
+
import { resolveReference } from './config.js';
|
|
4
|
+
export function llamaEndpoint(base, operation) {
|
|
5
|
+
const url = new URL(base);
|
|
6
|
+
const path = url.pathname.replace(/\/$/, '');
|
|
7
|
+
url.pathname = `${path}${path.endsWith('/v1') ? '' : '/v1'}/${operation}`;
|
|
8
|
+
return url;
|
|
9
|
+
}
|
|
10
|
+
const ToolCall = z.object({ id: z.string().min(1), type: z.literal('function'), function: z.object({ name: z.string().min(1), arguments: z.string() }) });
|
|
11
|
+
const FullResponse = z.object({ choices: z.array(z.object({ message: z.object({ content: z.string().nullable().optional(), reasoning_content: z.string().optional(), tool_calls: z.array(ToolCall).optional() }) })).min(1), usage: z.object({ prompt_tokens: z.number().optional(), completion_tokens: z.number().optional() }).optional() });
|
|
12
|
+
const Delta = z.object({ choices: z.array(z.object({ delta: z.object({ content: z.string().nullable().optional(), reasoning_content: z.string().nullable().optional(), tool_calls: z.array(z.object({ index: z.number().int().nonnegative(), id: z.string().optional(), type: z.literal('function').optional(), function: z.object({ name: z.string().optional(), arguments: z.string().optional() }).optional() })).optional() }) })), usage: z.object({ prompt_tokens: z.number().optional(), completion_tokens: z.number().optional() }).nullable().optional() });
|
|
13
|
+
const Models = z.object({ data: z.array(z.object({ id: z.string(), aliases: z.array(z.string()).optional(), meta: z.object({ n_ctx: z.number().int().positive().optional() }).passthrough().optional() }).passthrough()) }).passthrough();
|
|
14
|
+
const Props = z.object({ model_alias: z.string().optional(), default_generation_settings: z.object({ n_ctx: z.number().int().positive().optional(), params: z.object({ temperature: z.number().optional(), top_p: z.number().optional(), reasoning_format: z.string().optional(), chat_format: z.string().optional() }).passthrough().optional() }).passthrough().optional(), chat_template_caps: z.object({ supports_tools: z.boolean().optional(), supports_tool_calls: z.boolean().optional(), supports_reasoning_effort: z.boolean().optional() }).passthrough().optional() }).passthrough();
|
|
15
|
+
function readUsage(value) {
|
|
16
|
+
return { ...(value.prompt_tokens === undefined ? {} : { promptTokens: value.prompt_tokens }), ...(value.completion_tokens === undefined ? {} : { completionTokens: value.completion_tokens }) };
|
|
17
|
+
}
|
|
18
|
+
export async function* sseData(stream) {
|
|
19
|
+
const reader = stream.getReader();
|
|
20
|
+
const decoder = new TextDecoder();
|
|
21
|
+
let pending = '';
|
|
22
|
+
try {
|
|
23
|
+
while (true) {
|
|
24
|
+
const part = await reader.read();
|
|
25
|
+
if (part.done)
|
|
26
|
+
break;
|
|
27
|
+
pending = (pending + decoder.decode(part.value, { stream: true })).replace(/\r\n/g, '\n');
|
|
28
|
+
let boundary;
|
|
29
|
+
while ((boundary = pending.indexOf('\n\n')) >= 0) {
|
|
30
|
+
const event = pending.slice(0, boundary);
|
|
31
|
+
pending = pending.slice(boundary + 2);
|
|
32
|
+
const lines = event.split('\n').filter(line => line.startsWith('data:'));
|
|
33
|
+
if (lines.length)
|
|
34
|
+
yield lines.map(line => line.slice(5).trimStart()).join('\n');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
pending += decoder.decode();
|
|
38
|
+
if (pending.trim())
|
|
39
|
+
throw new Error('Incomplete SSE event');
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
reader.releaseLock();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export class NativeLlmBackend {
|
|
46
|
+
config;
|
|
47
|
+
env;
|
|
48
|
+
#discovery;
|
|
49
|
+
constructor(config, env = process.env) {
|
|
50
|
+
this.config = config;
|
|
51
|
+
this.env = env;
|
|
52
|
+
}
|
|
53
|
+
discover() {
|
|
54
|
+
this.#discovery ??= this.#discover();
|
|
55
|
+
return this.#discovery;
|
|
56
|
+
}
|
|
57
|
+
async countInputTokens(messages, tools, signal) {
|
|
58
|
+
const base = resolveReference(this.config.base_url, this.env);
|
|
59
|
+
const model = resolveReference(this.config.model, this.env);
|
|
60
|
+
if (!base || !model)
|
|
61
|
+
throw new Error('llama.cpp endpoint/model is unconfigured');
|
|
62
|
+
const token = this.env[this.config.api_key_env];
|
|
63
|
+
try {
|
|
64
|
+
const response = await fetch(llamaEndpoint(base, 'chat/completions/input_tokens'), {
|
|
65
|
+
method: 'POST', redirect: 'manual', signal: AbortSignal.any([signal, AbortSignal.timeout(5000)]),
|
|
66
|
+
headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
|
|
67
|
+
body: JSON.stringify({ model, messages, tools })
|
|
68
|
+
});
|
|
69
|
+
if (response.status === 404 || response.status === 405)
|
|
70
|
+
return null;
|
|
71
|
+
if (!response.ok)
|
|
72
|
+
throw new Error(`llama.cpp token count HTTP ${response.status}`);
|
|
73
|
+
return z.object({ input_tokens: z.number().int().nonnegative() }).parse(await response.json()).input_tokens;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (signal.aborted)
|
|
77
|
+
throw error;
|
|
78
|
+
return null; // Older servers lack token counting; the bounded local estimator remains active.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async #discover() {
|
|
82
|
+
const base = resolveReference(this.config.base_url, this.env);
|
|
83
|
+
const model = resolveReference(this.config.model, this.env);
|
|
84
|
+
if (!base || !model)
|
|
85
|
+
throw new Error('llama.cpp endpoint/model is unconfigured');
|
|
86
|
+
const token = this.env[this.config.api_key_env];
|
|
87
|
+
const headers = token ? { Authorization: `Bearer ${token}` } : {};
|
|
88
|
+
const get = async (url) => {
|
|
89
|
+
const response = await fetch(url, { headers, redirect: 'manual', signal: AbortSignal.timeout(2000) });
|
|
90
|
+
if (!response.ok)
|
|
91
|
+
throw new Error(`HTTP ${response.status}`);
|
|
92
|
+
return await response.json();
|
|
93
|
+
};
|
|
94
|
+
const root = new URL(base);
|
|
95
|
+
root.pathname = root.pathname.replace(/\/v1\/?$/, '/');
|
|
96
|
+
root.search = '';
|
|
97
|
+
root.hash = '';
|
|
98
|
+
const [modelsRaw, propsRaw] = await Promise.allSettled([get(llamaEndpoint(base, 'models')), get(new URL('props', root))]);
|
|
99
|
+
const models = modelsRaw.status === 'fulfilled' ? Models.safeParse(modelsRaw.value) : undefined;
|
|
100
|
+
const props = propsRaw.status === 'fulfilled' ? Props.safeParse(propsRaw.value) : undefined;
|
|
101
|
+
const entries = models?.success ? models.data.data : undefined;
|
|
102
|
+
const selected = entries?.find(item => item.id === model || item.aliases?.includes(model));
|
|
103
|
+
const settings = props?.success ? props.data.default_generation_settings : undefined;
|
|
104
|
+
const caps = props?.success ? props.data.chat_template_caps : undefined;
|
|
105
|
+
return { modelListed: entries ? Boolean(selected) : null, contextTokens: this.config.context_window_tokens ?? selected?.meta?.n_ctx ?? settings?.n_ctx ?? null,
|
|
106
|
+
supportsTools: caps?.supports_tools === false || caps?.supports_tool_calls === false ? false : caps?.supports_tools && caps.supports_tool_calls ? true : null,
|
|
107
|
+
supportsReasoningEffort: caps?.supports_reasoning_effort ?? null,
|
|
108
|
+
sampling: { temperature: settings?.params?.temperature ?? null, topP: settings?.params?.top_p ?? null },
|
|
109
|
+
reasoningFormat: settings?.params?.reasoning_format ?? null, chatFormat: settings?.params?.chat_format ?? null };
|
|
110
|
+
}
|
|
111
|
+
async complete(messages, tools, signal, stream = true, onText, onEvent) {
|
|
112
|
+
const base = resolveReference(this.config.base_url, this.env);
|
|
113
|
+
const model = resolveReference(this.config.model, this.env);
|
|
114
|
+
if (!base || !model)
|
|
115
|
+
throw new Error('llama.cpp endpoint/model is unconfigured');
|
|
116
|
+
const endpoint = llamaEndpoint(base, 'chat/completions');
|
|
117
|
+
const controller = new AbortController();
|
|
118
|
+
const timer = setTimeout(() => controller.abort(), this.config.request_timeout_seconds * 1000);
|
|
119
|
+
const abort = () => controller.abort();
|
|
120
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
121
|
+
try {
|
|
122
|
+
if (signal.aborted)
|
|
123
|
+
controller.abort();
|
|
124
|
+
const token = this.env[this.config.api_key_env];
|
|
125
|
+
const capabilities = this.config.reasoning_effort ? await this.discover() : undefined;
|
|
126
|
+
if (this.config.reasoning_effort && capabilities?.supportsReasoningEffort !== true)
|
|
127
|
+
throw new Error('llama.cpp reasoning effort is not advertised by the current chat template');
|
|
128
|
+
if (tools.length && capabilities?.supportsTools === false)
|
|
129
|
+
throw new Error('llama.cpp chat template does not support tools');
|
|
130
|
+
onEvent?.({ kind: 'started', backend: 'llamacpp' });
|
|
131
|
+
let response;
|
|
132
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
133
|
+
try {
|
|
134
|
+
response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: stream ? 'text/event-stream' : 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) }, body: JSON.stringify({ model, messages, tools, stream, max_tokens: this.config.max_output_tokens ?? 4096, ...(this.config.reasoning_effort ? { reasoning_effort: this.config.reasoning_effort } : {}), ...(stream ? { stream_options: { include_usage: true } } : {}) }), redirect: 'manual', signal: controller.signal });
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
if (attempt || controller.signal.aborted || signal.aborted)
|
|
138
|
+
throw error;
|
|
139
|
+
await delay(200, undefined, { signal: controller.signal });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (attempt === 0 && (response.status === 429 || response.status === 503)) {
|
|
143
|
+
await response.body?.cancel();
|
|
144
|
+
const retryAfter = Number(response.headers.get('retry-after'));
|
|
145
|
+
const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(2000, retryAfter * 1000) : 200;
|
|
146
|
+
await delay(waitMs, undefined, { signal: controller.signal });
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
if (!response)
|
|
152
|
+
throw new Error('llama.cpp transport failed before response');
|
|
153
|
+
if (!response.ok)
|
|
154
|
+
throw new Error(`llama.cpp HTTP ${response.status}`);
|
|
155
|
+
if (!stream) {
|
|
156
|
+
const raw = await response.json();
|
|
157
|
+
const parsed = FullResponse.parse(raw);
|
|
158
|
+
const message = parsed.choices[0]?.message;
|
|
159
|
+
if (!message)
|
|
160
|
+
throw new Error('llama.cpp returned no message');
|
|
161
|
+
if (message.content) {
|
|
162
|
+
onText?.(message.content);
|
|
163
|
+
onEvent?.({ kind: 'text_delta', backend: 'llamacpp', text: message.content });
|
|
164
|
+
}
|
|
165
|
+
const reply = { content: message.content ?? '', ...(message.reasoning_content ? { reasoning: message.reasoning_content } : {}), toolCalls: message.tool_calls ?? [], ...(parsed.usage ? { usage: readUsage(parsed.usage) } : {}) };
|
|
166
|
+
onEvent?.({ kind: 'completed', backend: 'llamacpp', output_chars: reply.content.length, tool_calls: reply.toolCalls.length, reasoning_chars: reply.reasoning?.length ?? 0, usage: reply.usage ?? null });
|
|
167
|
+
return reply;
|
|
168
|
+
}
|
|
169
|
+
if (!response.body)
|
|
170
|
+
throw new Error('llama.cpp returned no SSE stream');
|
|
171
|
+
let content = '';
|
|
172
|
+
let reasoning = '';
|
|
173
|
+
let usage;
|
|
174
|
+
let done = false;
|
|
175
|
+
const calls = new Map();
|
|
176
|
+
for await (const data of sseData(response.body)) {
|
|
177
|
+
if (data === '[DONE]') {
|
|
178
|
+
done = true;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
const raw = JSON.parse(data);
|
|
182
|
+
const event = Delta.parse(raw);
|
|
183
|
+
if (event.usage)
|
|
184
|
+
usage = readUsage(event.usage);
|
|
185
|
+
for (const choice of event.choices) {
|
|
186
|
+
content += choice.delta.content ?? '';
|
|
187
|
+
if (choice.delta.content) {
|
|
188
|
+
onText?.(choice.delta.content);
|
|
189
|
+
onEvent?.({ kind: 'text_delta', backend: 'llamacpp', text: choice.delta.content });
|
|
190
|
+
}
|
|
191
|
+
reasoning += choice.delta.reasoning_content ?? '';
|
|
192
|
+
for (const call of choice.delta.tool_calls ?? []) {
|
|
193
|
+
const prior = calls.get(call.index) ?? { id: '', name: '', arguments: '' };
|
|
194
|
+
if (call.id)
|
|
195
|
+
prior.id = call.id;
|
|
196
|
+
if (call.function?.name)
|
|
197
|
+
prior.name += call.function.name;
|
|
198
|
+
if (call.function?.arguments)
|
|
199
|
+
prior.arguments += call.function.arguments;
|
|
200
|
+
calls.set(call.index, prior);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (!done)
|
|
205
|
+
throw new Error('llama.cpp SSE stream ended before [DONE]');
|
|
206
|
+
const toolCalls = [...calls.entries()].sort(([a], [b]) => a - b).map(([, value]) => ToolCall.parse({ id: value.id, type: 'function', function: { name: value.name, arguments: value.arguments } }));
|
|
207
|
+
onEvent?.({ kind: 'completed', backend: 'llamacpp', output_chars: content.length, tool_calls: toolCalls.length, reasoning_chars: reasoning.length, usage: usage ?? null });
|
|
208
|
+
return { content, ...(reasoning ? { reasoning } : {}), toolCalls, ...(usage ? { usage } : {}) };
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
clearTimeout(timer);
|
|
212
|
+
signal.removeEventListener('abort', abort);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
export { NativeLlmBackend as LlamaCppBackend };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineTool } from './tools.js';
|
|
3
|
+
import { magnusFleetReadinessTool } from './magnus-readiness.js';
|
|
4
|
+
import { magnusNodeDependencyImpactTool } from './magnus-node-dependency-impact.js';
|
|
5
|
+
import { readOwnedApi } from './owner-read.js';
|
|
6
|
+
import { magnusOwnerReadTools } from './magnus-owner-reads.js';
|
|
7
|
+
const id = z.string().regex(/^[A-Za-z0-9_-]{1,128}$/);
|
|
8
|
+
const node = z.object({ id: z.string(), name: z.string(), hostname: z.string().nullable().optional(), status: z.string() }).passthrough();
|
|
9
|
+
const count = z.number().int().nonnegative();
|
|
10
|
+
const anomalySummary = z.object({
|
|
11
|
+
totalActive: count,
|
|
12
|
+
byNode: z.array(z.object({ nodeId: id, nodeName: z.string().max(160), count, metrics: z.array(z.string().max(80)).max(16) }).passthrough()).max(100),
|
|
13
|
+
bySeverity: z.object({ info: count, warning: count, critical: count }).passthrough()
|
|
14
|
+
}).passthrough();
|
|
15
|
+
const commandStatus = z.object({
|
|
16
|
+
id, nodeId: id,
|
|
17
|
+
status: z.enum(['PENDING', 'QUEUED', 'RUNNING', 'COMPLETED', 'FAILED', 'TIMEOUT', 'CANCELLED']),
|
|
18
|
+
createdAt: z.string().datetime(), startedAt: z.string().datetime().nullable(), completedAt: z.string().datetime().nullable(),
|
|
19
|
+
result: z.object({ exitCode: z.number().int(), durationMs: count, truncated: z.boolean() }).passthrough().nullable()
|
|
20
|
+
}).passthrough();
|
|
21
|
+
// The owning agent reports health with its 30-second heartbeat. This matches
|
|
22
|
+
// the Control API's default stale-node threshold, but is a tool-local label:
|
|
23
|
+
// an operator may configure a different server-side threshold.
|
|
24
|
+
const HEALTH_FRESHNESS_LIMIT_MS = 120_000;
|
|
25
|
+
const HEALTH_FUTURE_SKEW_MS = 5_000;
|
|
26
|
+
/** Optional read-only Magnus Control tools. The base URL and token come from the environment only. */
|
|
27
|
+
export function registerMagnusControlTools(registry, env) {
|
|
28
|
+
registry.register(magnusFleetReadinessTool(env));
|
|
29
|
+
registry.register(magnusNodeDependencyImpactTool(env));
|
|
30
|
+
for (const tool of magnusOwnerReadTools(env))
|
|
31
|
+
registry.register(tool);
|
|
32
|
+
registry.register(defineTool({
|
|
33
|
+
name: 'magnus_fleet_inventory', description: 'Read bounded fleet inventory from the owning Magnus Control API. Observations do not authorize commands or remediation.',
|
|
34
|
+
input: z.object({}).strict(),
|
|
35
|
+
output: z.object({ source: z.string().url(), source_date: z.string().datetime().nullable(), observed_at: z.string().datetime(), nodes: z.array(z.object({ id: z.string(), name: z.string(), hostname: z.string().nullable(), status: z.string() }).strict()).max(100) }).strict(),
|
|
36
|
+
effect: 'read', contexts: ['direct', 'peer'], timeoutMs: 15_000,
|
|
37
|
+
handler: async (_input, signal) => {
|
|
38
|
+
const { value, source, sourceDate } = await readOwnedApi('ARDENT_MAGNUS_API_URL', 'ARDENT_MAGNUS_READ_TOKEN', '/api/v1/nodes', env, signal);
|
|
39
|
+
const nodes = z.array(node).max(100).parse(value);
|
|
40
|
+
return { source, source_date: sourceDate, observed_at: new Date().toISOString(), nodes: nodes.map(item => ({ id: item.id, name: item.name, hostname: item.hostname ?? null, status: item.status })) };
|
|
41
|
+
}
|
|
42
|
+
}));
|
|
43
|
+
registry.register(defineTool({
|
|
44
|
+
name: 'magnus_node_health', description: 'Read the latest retained Magnus health snapshot for one inventory node with an explicit freshness label. A stale snapshot is historical evidence, not current health. This does not authorize a node command.',
|
|
45
|
+
input: z.object({ node_id: id }).strict(),
|
|
46
|
+
output: z.object({ source: z.string().url(), source_date: z.string().datetime().nullable(), observed_at: z.string().datetime(), node_id: id,
|
|
47
|
+
snapshot_at: z.string().datetime().nullable(), snapshot_age_ms: z.number().int().nullable(),
|
|
48
|
+
snapshot_freshness: z.enum(['fresh', 'stale', 'future', 'missing']), freshness_limit_ms: z.literal(HEALTH_FRESHNESS_LIMIT_MS),
|
|
49
|
+
cpu_usage_percent: z.number().nullable(), memory_percent: z.number().nullable() }).strict(),
|
|
50
|
+
effect: 'read', contexts: ['direct', 'peer'], timeoutMs: 15_000,
|
|
51
|
+
handler: async ({ node_id }, signal) => {
|
|
52
|
+
const { value, source, sourceDate } = await readOwnedApi('ARDENT_MAGNUS_API_URL', 'ARDENT_MAGNUS_READ_TOKEN', `/api/v1/nodes/${node_id}/health`, env, signal);
|
|
53
|
+
const data = z.object({ nodeId: z.string().optional(), timestamp: z.string().datetime().optional(), cpuUsagePercent: z.number().nullable().optional(), memoryPercent: z.number().nullable().optional() }).passthrough().parse(value);
|
|
54
|
+
if (data.nodeId && data.nodeId !== node_id)
|
|
55
|
+
throw Object.assign(new Error('Health snapshot node identity differs from the request'), { code: 'SOURCE_IDENTITY_MISMATCH' });
|
|
56
|
+
const observedAt = Date.now();
|
|
57
|
+
const snapshotAgeMs = data.timestamp ? observedAt - Date.parse(data.timestamp) : null;
|
|
58
|
+
const snapshotFreshness = snapshotAgeMs === null ? 'missing' : snapshotAgeMs < -HEALTH_FUTURE_SKEW_MS ? 'future'
|
|
59
|
+
: snapshotAgeMs > HEALTH_FRESHNESS_LIMIT_MS ? 'stale' : 'fresh';
|
|
60
|
+
return { source, source_date: sourceDate, observed_at: new Date(observedAt).toISOString(), node_id,
|
|
61
|
+
snapshot_at: data.timestamp ?? null, snapshot_age_ms: snapshotAgeMs,
|
|
62
|
+
snapshot_freshness: snapshotFreshness, freshness_limit_ms: HEALTH_FRESHNESS_LIMIT_MS,
|
|
63
|
+
cpu_usage_percent: data.cpuUsagePercent ?? null, memory_percent: data.memoryPercent ?? null };
|
|
64
|
+
}
|
|
65
|
+
}));
|
|
66
|
+
registry.register(defineTool({
|
|
67
|
+
name: 'magnus_anomaly_summary', description: 'Read bounded active fleet anomaly counts and affected nodes from Magnus. This is an observation, not remediation authority.',
|
|
68
|
+
input: z.object({}).strict(),
|
|
69
|
+
output: z.object({ source: z.string().url(), source_date: z.string().datetime().nullable(), observed_at: z.string().datetime(),
|
|
70
|
+
total_active: count, by_severity: z.object({ info: count, warning: count, critical: count }).strict(),
|
|
71
|
+
nodes: z.array(z.object({ node_id: id, node_name: z.string().max(160), count, metrics: z.array(z.string().max(80)).max(16) }).strict()).max(100) }).strict(),
|
|
72
|
+
effect: 'read', contexts: ['direct', 'peer'], timeoutMs: 15_000,
|
|
73
|
+
handler: async (_input, signal) => {
|
|
74
|
+
const { value, source, sourceDate } = await readOwnedApi('ARDENT_MAGNUS_API_URL', 'ARDENT_MAGNUS_READ_TOKEN', '/api/v1/anomalies/summary', env, signal);
|
|
75
|
+
const data = anomalySummary.parse(value);
|
|
76
|
+
return { source, source_date: sourceDate, observed_at: new Date().toISOString(), total_active: data.totalActive,
|
|
77
|
+
by_severity: { info: data.bySeverity.info, warning: data.bySeverity.warning, critical: data.bySeverity.critical },
|
|
78
|
+
nodes: data.byNode.map(item => ({ node_id: item.nodeId, node_name: item.nodeName, count: item.count, metrics: item.metrics })) };
|
|
79
|
+
}
|
|
80
|
+
}));
|
|
81
|
+
registry.register(defineTool({
|
|
82
|
+
name: 'magnus_command_status', description: 'Read one Magnus command lifecycle status by exact ID. Command text, arguments, environment, and output are excluded.',
|
|
83
|
+
input: z.object({ command_id: id }).strict(),
|
|
84
|
+
output: z.object({ source: z.string().url(), source_date: z.string().datetime().nullable(), observed_at: z.string().datetime(),
|
|
85
|
+
command_id: id, node_id: id, status: commandStatus.shape.status,
|
|
86
|
+
created_at: z.string().datetime(), started_at: z.string().datetime().nullable(), completed_at: z.string().datetime().nullable(),
|
|
87
|
+
exit_code: z.number().int().nullable(), duration_ms: count.nullable(), result_truncated: z.boolean().nullable() }).strict(),
|
|
88
|
+
effect: 'read', contexts: ['direct', 'peer'], timeoutMs: 15_000,
|
|
89
|
+
handler: async ({ command_id }, signal) => {
|
|
90
|
+
const { value, source, sourceDate } = await readOwnedApi('ARDENT_MAGNUS_API_URL', 'ARDENT_MAGNUS_READ_TOKEN', `/api/v1/commands/${command_id}`, env, signal);
|
|
91
|
+
const data = commandStatus.parse(value);
|
|
92
|
+
if (data.id !== command_id)
|
|
93
|
+
throw Object.assign(new Error('Command identity differs from the request'), { code: 'SOURCE_IDENTITY_MISMATCH' });
|
|
94
|
+
return { source, source_date: sourceDate, observed_at: new Date().toISOString(), command_id: data.id, node_id: data.nodeId,
|
|
95
|
+
status: data.status, created_at: data.createdAt, started_at: data.startedAt, completed_at: data.completedAt,
|
|
96
|
+
exit_code: data.result?.exitCode ?? null, duration_ms: data.result?.durationMs ?? null, result_truncated: data.result?.truncated ?? null };
|
|
97
|
+
}
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function magnusNodeDependencyImpactTool(env: NodeJS.ProcessEnv): import("./tools.js").Tool;
|