@aiwg/cli 2026.8.17 → 2026.8.19
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/bin/aiwg.mjs +24 -2
- package/dist/src/a2a/agent-card.js +4 -1
- package/dist/src/a2a/client.js +148 -68
- package/dist/src/a2a/codecs.js +480 -0
- package/dist/src/a2a/events.js +226 -0
- package/dist/src/a2a/hitl-driver.js +8 -6
- package/dist/src/a2a/hitl.js +2 -1
- package/dist/src/a2a/http.js +85 -5
- package/dist/src/a2a/protocol.js +136 -0
- package/dist/src/a2a/types.js +4 -14
- package/dist/src/a2a/webhook.js +101 -4
- package/dist/src/artifacts/index-builder.js +63 -12
- package/dist/src/artifacts/index-files.js +26 -5
- package/dist/src/artifacts/query-engine.js +67 -67
- package/dist/src/artifacts/stats.js +6 -2
- package/dist/src/artifacts/types.js +1 -1
- package/dist/src/audit/operator-decision.js +15 -1
- package/dist/src/channel/manager.mjs +89 -17
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/installation.js +79 -0
- package/dist/src/cli/handlers/refresh.js +6 -2
- package/dist/src/cli/handlers/runtime-info.js +9 -1
- package/dist/src/cli/handlers/serve.js +107 -4
- package/dist/src/cli/handlers/session.js +12 -26
- package/dist/src/cli/handlers/use.js +19 -4
- package/dist/src/cli/handlers/utilities.js +4 -0
- package/dist/src/cli/handlers/version.js +4 -0
- package/dist/src/config/user-config-dir.mjs +29 -0
- package/dist/src/config/user-config.js +4 -22
- package/dist/src/extensions/commands/definitions.js +20 -1
- package/dist/src/features/catalog.js +2 -1
- package/dist/src/flow/graph-metadata.js +56 -0
- package/dist/src/installation/manager-command.mjs +31 -0
- package/dist/src/installation/manager.mjs +264 -0
- package/dist/src/serve/a2a-terminal-observer.js +28 -5
- package/dist/src/serve/dispatch-router.js +32 -4
- package/dist/src/serve/executor-registry.js +29 -0
- package/dist/src/serve/mission-conductor.js +15 -1
- package/dist/src/serve/stack-adapters.js +2 -1
- package/dist/src/serve/telemetry.js +5 -1
- package/dist/src/smiths/context-pipeline/claude-hook.js +8 -5
- package/dist/src/smiths/context-pipeline/line-endings.js +12 -0
- package/dist/src/smiths/context-pipeline/managed-hook.js +8 -5
- package/dist/src/smiths/context-pipeline/workspace-context.js +3 -1
- package/dist/src/update/checker.mjs +16 -15
- package/dist/src/update/notifier.mjs +8 -3
- package/dist/src/update/service.mjs +51 -5
- package/package.json +1 -1
- package/tools/agents/deploy-agents.mjs +28 -8
- package/tools/agents/providers/base.mjs +5 -3
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import {
|
|
3
|
+
accessSync,
|
|
4
|
+
constants,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
realpathSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
statSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { resolveUserConfigDir } from '../config/user-config-dir.mjs';
|
|
15
|
+
import { executeManagerCommand } from './manager-command.mjs';
|
|
16
|
+
|
|
17
|
+
export const INSTALLATION_IDENTITY_VERSION = 1;
|
|
18
|
+
export const INSTALLATION_FILE = 'installation.json';
|
|
19
|
+
const METHODS = new Set(['npm', 'web', 'source']);
|
|
20
|
+
const RUN_MODES = new Set(['normal', 'development']);
|
|
21
|
+
const CHANNELS = new Set(['stable', 'next', 'nightly', 'edge']);
|
|
22
|
+
const STRATEGIES = new Set(['npm-global', 'signed-web', 'source-git']);
|
|
23
|
+
|
|
24
|
+
function canonicalPath(value) {
|
|
25
|
+
const resolved = path.resolve(value);
|
|
26
|
+
try { return realpathSync.native(resolved); } catch { return resolved; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function packageName(root) {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8')).name ?? null;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function inferInstallationMethod(root) {
|
|
38
|
+
if (packageName(root) === '@aiwg/cli') return 'web';
|
|
39
|
+
if (existsSync(path.join(root, '.git'))) return 'source';
|
|
40
|
+
return 'npm';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolveExecutable(name, options = {}) {
|
|
44
|
+
const env = options.env ?? process.env;
|
|
45
|
+
const explicit = options.managerExecutable ?? env.AIWG_PACKAGE_MANAGER_EXECUTABLE;
|
|
46
|
+
if (explicit) return canonicalPath(explicit);
|
|
47
|
+
if (name === 'npm' && env.npm_execpath) return canonicalPath(env.npm_execpath);
|
|
48
|
+
|
|
49
|
+
const besideNode = path.join(path.dirname(process.execPath), process.platform === 'win32' ? `${name}.cmd` : name);
|
|
50
|
+
if (existsSync(besideNode)) return canonicalPath(besideNode);
|
|
51
|
+
try {
|
|
52
|
+
const finder = process.platform === 'win32' ? 'where.exe' : 'which';
|
|
53
|
+
const found = execFileSync(finder, [name], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
|
|
54
|
+
.split(/\r?\n/, 1)[0]?.trim();
|
|
55
|
+
return found ? canonicalPath(found) : null;
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function strategyFor(method) {
|
|
62
|
+
if (method === 'web') return 'signed-web';
|
|
63
|
+
if (method === 'source') return 'source-git';
|
|
64
|
+
return 'npm-global';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function executableIsUsable(file) {
|
|
68
|
+
try {
|
|
69
|
+
if (!statSync(file).isFile()) return false;
|
|
70
|
+
if (process.platform !== 'win32') accessSync(file, constants.X_OK);
|
|
71
|
+
return true;
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function validateIdentity(value, file) {
|
|
78
|
+
const invalid = !value || typeof value !== 'object'
|
|
79
|
+
|| value.schemaVersion !== INSTALLATION_IDENTITY_VERSION
|
|
80
|
+
|| !METHODS.has(value.method)
|
|
81
|
+
|| !RUN_MODES.has(value.runMode)
|
|
82
|
+
|| !CHANNELS.has(value.channel)
|
|
83
|
+
|| typeof value.root !== 'string'
|
|
84
|
+
|| !path.isAbsolute(value.root)
|
|
85
|
+
|| !STRATEGIES.has(value.updateStrategy)
|
|
86
|
+
|| value.updateStrategy !== strategyFor(value.method)
|
|
87
|
+
|| (value.managerExecutable !== null
|
|
88
|
+
&& (typeof value.managerExecutable !== 'string' || !path.isAbsolute(value.managerExecutable)));
|
|
89
|
+
if (invalid) {
|
|
90
|
+
const error = new Error(`Invalid AIWG installation identity at ${file}. Run \`aiwg installation adopt\` to replace it.`);
|
|
91
|
+
error.code = 'AIWG_INSTALLATION_INVALID';
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function installationFile(options = {}) {
|
|
98
|
+
return path.join(resolveUserConfigDir(options), INSTALLATION_FILE);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function createInstallationIdentity(options) {
|
|
102
|
+
if (!options?.actualRoot) throw new Error('actualRoot is required to create an installation identity');
|
|
103
|
+
const root = canonicalPath(options.root ?? options.actualRoot);
|
|
104
|
+
const method = options.method ?? inferInstallationMethod(root);
|
|
105
|
+
const runMode = options.runMode ?? (method === 'source' ? 'development' : 'normal');
|
|
106
|
+
const requestedChannel = options.channel ?? (runMode === 'development' ? 'edge' : 'stable');
|
|
107
|
+
const channel = requestedChannel === 'latest'
|
|
108
|
+
? 'stable'
|
|
109
|
+
: ['alpha', 'beta', 'rc'].includes(requestedChannel) ? 'next' : requestedChannel;
|
|
110
|
+
const executableName = method === 'npm' ? 'npm' : method === 'source' ? 'git' : null;
|
|
111
|
+
return {
|
|
112
|
+
schemaVersion: INSTALLATION_IDENTITY_VERSION,
|
|
113
|
+
runMode,
|
|
114
|
+
method,
|
|
115
|
+
root,
|
|
116
|
+
updateStrategy: options.updateStrategy ?? strategyFor(method),
|
|
117
|
+
managerExecutable: executableName ? resolveExecutable(executableName, options) : null,
|
|
118
|
+
channel,
|
|
119
|
+
edgePath: options.edgePath ? canonicalPath(options.edgePath) : (runMode === 'development' ? root : null),
|
|
120
|
+
checkOnStartup: options.checkOnStartup ?? true,
|
|
121
|
+
lastUpdateCheck: options.lastUpdateCheck ?? null,
|
|
122
|
+
updateCheckInterval: options.updateCheckInterval ?? 86_400_000,
|
|
123
|
+
recordedAt: options.recordedAt ?? new Date().toISOString(),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function saveInstallationIdentity(identity, options = {}) {
|
|
128
|
+
const file = installationFile(options);
|
|
129
|
+
const validated = validateIdentity(identity, file);
|
|
130
|
+
mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
131
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
132
|
+
writeFileSync(temporary, `${JSON.stringify(validated, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
133
|
+
renameSync(temporary, file);
|
|
134
|
+
return validated;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function readLegacy(options = {}) {
|
|
138
|
+
const file = path.join(resolveUserConfigDir(options), 'channel.json');
|
|
139
|
+
try { return JSON.parse(readFileSync(file, 'utf8')); } catch { return null; }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Load the canonical record, migrating legacy channel.json on first access. */
|
|
143
|
+
export function loadInstallationIdentity(options = {}) {
|
|
144
|
+
const file = installationFile(options);
|
|
145
|
+
if (existsSync(file)) {
|
|
146
|
+
try { return validateIdentity(JSON.parse(readFileSync(file, 'utf8')), file); }
|
|
147
|
+
catch (error) {
|
|
148
|
+
if (error?.code === 'AIWG_INSTALLATION_INVALID') throw error;
|
|
149
|
+
const wrapped = new Error(`Cannot read AIWG installation identity at ${file}: ${error.message}`);
|
|
150
|
+
wrapped.code = 'AIWG_INSTALLATION_INVALID';
|
|
151
|
+
throw wrapped;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (options.createIfMissing === false) return null;
|
|
155
|
+
if (!options.actualRoot) return null;
|
|
156
|
+
|
|
157
|
+
const legacy = options.legacyConfig ?? readLegacy(options) ?? {};
|
|
158
|
+
const development = legacy.devMode === true;
|
|
159
|
+
const root = development && legacy.edgePath ? legacy.edgePath : options.actualRoot;
|
|
160
|
+
const method = options.method ?? (development ? 'source' : inferInstallationMethod(root));
|
|
161
|
+
const identity = createInstallationIdentity({
|
|
162
|
+
...options,
|
|
163
|
+
root,
|
|
164
|
+
method,
|
|
165
|
+
runMode: development ? 'development' : undefined,
|
|
166
|
+
channel: legacy.channel ?? options.channel,
|
|
167
|
+
edgePath: legacy.edgePath,
|
|
168
|
+
lastUpdateCheck: legacy.lastUpdateCheck,
|
|
169
|
+
updateCheckInterval: legacy.updateCheckInterval,
|
|
170
|
+
checkOnStartup: legacy.checkOnStartup,
|
|
171
|
+
});
|
|
172
|
+
return saveInstallationIdentity(identity, options);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function inspectInstallation(options = {}) {
|
|
176
|
+
const actualRoot = canonicalPath(options.actualRoot);
|
|
177
|
+
const actualMethod = options.actualMethod ?? inferInstallationMethod(actualRoot);
|
|
178
|
+
const identity = options.identity ?? loadInstallationIdentity({ ...options, actualRoot });
|
|
179
|
+
if (!identity) return { state: 'unrecorded', identity: null, actualRoot, actualMethod, drift: ['installation identity is not recorded'] };
|
|
180
|
+
|
|
181
|
+
const drift = [];
|
|
182
|
+
const canonicalRoot = canonicalPath(identity.root);
|
|
183
|
+
if (!existsSync(canonicalRoot)) drift.push(`canonical root does not exist: ${canonicalRoot}`);
|
|
184
|
+
if (canonicalRoot !== actualRoot) drift.push(`actual root ${actualRoot} differs from canonical root ${canonicalRoot}`);
|
|
185
|
+
if (identity.method !== actualMethod) drift.push(`actual method ${actualMethod} differs from canonical method ${identity.method}`);
|
|
186
|
+
if (identity.method !== 'web' && !identity.managerExecutable) {
|
|
187
|
+
drift.push(`canonical ${identity.method} installation has no recorded manager executable`);
|
|
188
|
+
}
|
|
189
|
+
if (identity.managerExecutable && !executableIsUsable(identity.managerExecutable)) {
|
|
190
|
+
drift.push(`recorded manager executable is missing or not executable: ${identity.managerExecutable}`);
|
|
191
|
+
}
|
|
192
|
+
let managerProbe = null;
|
|
193
|
+
if (
|
|
194
|
+
options.probeManager === true &&
|
|
195
|
+
identity.managerExecutable &&
|
|
196
|
+
executableIsUsable(identity.managerExecutable)
|
|
197
|
+
) {
|
|
198
|
+
try {
|
|
199
|
+
executeManagerCommand(identity.managerExecutable, ['--version'], {
|
|
200
|
+
...options,
|
|
201
|
+
execute: options.executeManager,
|
|
202
|
+
execOptions: { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000 },
|
|
203
|
+
});
|
|
204
|
+
managerProbe = { state: 'usable' };
|
|
205
|
+
} catch (error) {
|
|
206
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
207
|
+
managerProbe = { state: 'failed', error: message };
|
|
208
|
+
drift.push(`recorded manager executable cannot be invoked: ${message}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
state: drift.length === 0 ? 'aligned' : (existsSync(canonicalRoot) ? 'mismatch' : 'stale'),
|
|
213
|
+
identity,
|
|
214
|
+
canonicalRoot,
|
|
215
|
+
actualRoot,
|
|
216
|
+
actualMethod,
|
|
217
|
+
drift,
|
|
218
|
+
managerProbe,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function formatInstallationDiagnostic(status) {
|
|
223
|
+
if (status.state === 'aligned') return 'Canonical installation is aligned.';
|
|
224
|
+
return [
|
|
225
|
+
'AIWG installation identity drift detected; update and refresh are blocked.',
|
|
226
|
+
...status.drift.map((item) => `- ${item}`),
|
|
227
|
+
'Inspect: aiwg installation show',
|
|
228
|
+
'Adopt this installation: aiwg installation adopt',
|
|
229
|
+
'Switch deliberately: aiwg installation switch --root <path> --method <npm|web|source> [--manager <absolute-path>]',
|
|
230
|
+
].join('\n');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function assertCanonicalInstallation(options = {}) {
|
|
234
|
+
const status = inspectInstallation(options);
|
|
235
|
+
if (status.state !== 'aligned') {
|
|
236
|
+
const error = new Error(formatInstallationDiagnostic(status));
|
|
237
|
+
error.code = 'AIWG_INSTALLATION_DRIFT';
|
|
238
|
+
error.status = status;
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
return status;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function adoptInstallation(options) {
|
|
245
|
+
const inferred = inferInstallationMethod(options.actualRoot);
|
|
246
|
+
if (options.method && options.method !== inferred) {
|
|
247
|
+
throw new Error(`Cannot adopt ${options.actualRoot} as ${options.method}; package contents identify it as ${inferred}.`);
|
|
248
|
+
}
|
|
249
|
+
const identity = createInstallationIdentity({ ...options, root: options.actualRoot });
|
|
250
|
+
saveInstallationIdentity(identity, options);
|
|
251
|
+
return inspectInstallation({ ...options, actualRoot: identity.root, identity });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function switchInstallation(options) {
|
|
255
|
+
if (!options?.root || !options?.method) throw new Error('switch requires root and method');
|
|
256
|
+
if (!existsSync(path.resolve(options.root))) throw new Error(`Installation root does not exist: ${path.resolve(options.root)}`);
|
|
257
|
+
const inferred = inferInstallationMethod(options.root);
|
|
258
|
+
if (options.method !== inferred) {
|
|
259
|
+
throw new Error(`Cannot switch ${options.root} as ${options.method}; package contents identify it as ${inferred}.`);
|
|
260
|
+
}
|
|
261
|
+
const identity = createInstallationIdentity({ ...options, actualRoot: options.root });
|
|
262
|
+
saveInstallationIdentity(identity, options);
|
|
263
|
+
return inspectInstallation({ ...options, actualRoot: identity.root, identity });
|
|
264
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { A2AClient } from '../a2a/client.js';
|
|
12
12
|
import { isTerminalTaskState, } from '../a2a/types.js';
|
|
13
|
+
import { extractGraphMetadata } from '../flow/graph-metadata.js';
|
|
13
14
|
const DEFAULT_POLL_INTERVAL_MS = 1000;
|
|
14
15
|
const DEFAULT_MAX_POLLS = 300;
|
|
15
16
|
export async function observeA2ATerminalState(registry, executor, missionId, a2aInstanceId, initialTask, opts = {}) {
|
|
@@ -18,7 +19,11 @@ export async function observeA2ATerminalState(registry, executor, missionId, a2a
|
|
|
18
19
|
baseUrl: executor.transportEndpoints.rest,
|
|
19
20
|
bearer: executor.token,
|
|
20
21
|
instanceId: a2aInstanceId,
|
|
22
|
+
protocolVersion: opts.protocolVersion ?? '0.3',
|
|
23
|
+
protocolPolicy: opts.protocolVersion ?? '0.3',
|
|
21
24
|
};
|
|
25
|
+
if (opts.selectedInterface)
|
|
26
|
+
clientOpts.selectedInterface = opts.selectedInterface;
|
|
22
27
|
if (opts.fetch)
|
|
23
28
|
clientOpts.fetch = opts.fetch;
|
|
24
29
|
const client = new A2AClient(clientOpts);
|
|
@@ -90,21 +95,39 @@ function emitTerminalTask(registry, executorId, missionId, task) {
|
|
|
90
95
|
}));
|
|
91
96
|
}
|
|
92
97
|
function makeEnvelope(event, executorId, missionId, task, data) {
|
|
98
|
+
const graph = extractGraphMetadata(task.metadata);
|
|
93
99
|
return {
|
|
94
100
|
event,
|
|
95
101
|
executor_id: executorId,
|
|
96
102
|
mission_id: missionId,
|
|
97
103
|
ts: task.status.timestamp ?? new Date().toISOString(),
|
|
98
|
-
data
|
|
104
|
+
data: {
|
|
105
|
+
...data,
|
|
106
|
+
...(graph ? {
|
|
107
|
+
graph_metadata: { ...graph, nodeState: taskStateToGraphState(task.status.state) },
|
|
108
|
+
graph_node_state: taskStateToGraphState(task.status.state),
|
|
109
|
+
} : {}),
|
|
110
|
+
},
|
|
99
111
|
};
|
|
100
112
|
}
|
|
113
|
+
function taskStateToGraphState(state) {
|
|
114
|
+
switch (state) {
|
|
115
|
+
case 'submitted': return 'pending';
|
|
116
|
+
case 'working': return 'running';
|
|
117
|
+
case 'input-required':
|
|
118
|
+
case 'auth-required': return 'blocked-hitl';
|
|
119
|
+
case 'completed': return 'succeeded';
|
|
120
|
+
case 'failed':
|
|
121
|
+
case 'rejected': return 'failed';
|
|
122
|
+
case 'canceled': return 'canceled';
|
|
123
|
+
default: return 'unknown';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
101
126
|
function taskStatusSummary(task) {
|
|
102
|
-
|
|
103
|
-
return typeof status.summary === 'string' ? status.summary : undefined;
|
|
127
|
+
return task.status.summary;
|
|
104
128
|
}
|
|
105
129
|
function exitCodeData(task) {
|
|
106
|
-
|
|
107
|
-
return typeof status.exit_code === 'number' ? { exit_code: status.exit_code } : {};
|
|
130
|
+
return task.status.exitCode !== undefined ? { exit_code: task.status.exitCode } : {};
|
|
108
131
|
}
|
|
109
132
|
function sleep(ms) {
|
|
110
133
|
return new Promise((resolve) => {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* v1 payload → A2A Message
|
|
13
13
|
* -------------------------------- --------------------------------------
|
|
14
14
|
* mission_id message.messageId (idempotency key)
|
|
15
|
-
* objective parts[0] =
|
|
15
|
+
* objective parts[0] = normalized text content
|
|
16
16
|
* completion metadata.completion
|
|
17
17
|
* executor_filter metadata.executor_filter
|
|
18
18
|
* long_running metadata.long_running
|
|
@@ -39,7 +39,10 @@ export async function routeDispatch(executor, payload, opts = {}) {
|
|
|
39
39
|
}
|
|
40
40
|
catch (err) {
|
|
41
41
|
// Only fall back on a 404 from the v2 path. Everything else propagates.
|
|
42
|
-
|
|
42
|
+
const policy = opts.a2aProtocolPolicy ?? '0.3';
|
|
43
|
+
const allowLegacyFallback = policy !== '1.0'
|
|
44
|
+
&& (opts.allowLegacyExecutorFallback ?? true);
|
|
45
|
+
if (allowLegacyFallback && err instanceof A2AError && err.status === 404) {
|
|
43
46
|
// Capture sunset for the telemetry event if any was attached.
|
|
44
47
|
const sunset = err.problem.code === 'aiwg.deprecation_strict' ? undefined : undefined;
|
|
45
48
|
if (opts.onV1Fallback) {
|
|
@@ -62,6 +65,8 @@ async function dispatchV2(executor, payload, opts) {
|
|
|
62
65
|
bearer: executor.token,
|
|
63
66
|
instanceId: a2aInstanceId,
|
|
64
67
|
requiredExtensions: opts.requiredExtensions ?? [A2A_RUNTIME_V1, A2A_IDEMPOTENCY_V1],
|
|
68
|
+
protocolPolicy: opts.a2aProtocolPolicy ?? '0.3',
|
|
69
|
+
allowProtocolFallback: opts.allowA2AProtocolFallback ?? false,
|
|
65
70
|
};
|
|
66
71
|
if (opts.fetch)
|
|
67
72
|
clientOpts.fetch = opts.fetch;
|
|
@@ -69,7 +74,27 @@ async function dispatchV2(executor, payload, opts) {
|
|
|
69
74
|
clientOpts.optionalExtensions = opts.optionalExtensions;
|
|
70
75
|
if (opts.onDeprecation)
|
|
71
76
|
clientOpts.onDeprecation = opts.onDeprecation;
|
|
72
|
-
|
|
77
|
+
if (opts.onA2AProtocolFallback)
|
|
78
|
+
clientOpts.onProtocolFallback = opts.onA2AProtocolFallback;
|
|
79
|
+
if (opts.onA2AProtocolSelection) {
|
|
80
|
+
clientOpts.onProtocolSelection = info => opts.onA2AProtocolSelection?.(info);
|
|
81
|
+
}
|
|
82
|
+
// The deployed 0.3 compatibility route predates AgentCard negotiation. Model
|
|
83
|
+
// it as an explicit interface so headerless legacy selection is observable
|
|
84
|
+
// in registry, telemetry, audit, and dispatch results without adding a new
|
|
85
|
+
// discovery dependency to the compatibility path.
|
|
86
|
+
if (clientOpts.protocolPolicy === '0.3') {
|
|
87
|
+
clientOpts.selectedInterface = {
|
|
88
|
+
url: `${executor.transportEndpoints.rest.replace(/\/+$/, '')}/agents/${encodeURIComponent(a2aInstanceId)}`,
|
|
89
|
+
protocolBinding: 'REST',
|
|
90
|
+
protocolVersion: '0.3',
|
|
91
|
+
preference: 0,
|
|
92
|
+
legacy: true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const client = clientOpts.protocolPolicy === '0.3'
|
|
96
|
+
? new A2AClient(clientOpts)
|
|
97
|
+
: await A2AClient.negotiate(clientOpts);
|
|
73
98
|
const message = payloadToMessage(payload);
|
|
74
99
|
const result = await client.sendMessage(message);
|
|
75
100
|
return {
|
|
@@ -79,6 +104,9 @@ async function dispatchV2(executor, payload, opts) {
|
|
|
79
104
|
dispatchPath: 'v2',
|
|
80
105
|
task: result.task,
|
|
81
106
|
idempotentReplayed: result.idempotentReplayed,
|
|
107
|
+
a2aProtocolVersion: result.protocolVersion,
|
|
108
|
+
...(result.selectedInterface ? { a2aInterface: result.selectedInterface } : {}),
|
|
109
|
+
...(result.fallbackReason ? { a2aFallbackReason: result.fallbackReason } : {}),
|
|
82
110
|
};
|
|
83
111
|
}
|
|
84
112
|
function resolveA2AInstanceId(executor, payload, opts) {
|
|
@@ -145,7 +173,7 @@ function payloadToMessage(payload) {
|
|
|
145
173
|
return {
|
|
146
174
|
messageId: payload.mission_id,
|
|
147
175
|
role: 'user',
|
|
148
|
-
parts: [{
|
|
176
|
+
parts: [{ type: 'text', text: payload.objective }],
|
|
149
177
|
metadata,
|
|
150
178
|
};
|
|
151
179
|
}
|
|
@@ -232,6 +232,7 @@ export class ExecutorRegistry extends EventEmitter {
|
|
|
232
232
|
// Upsert — preserve token and registeredAt
|
|
233
233
|
existing.name = req.name;
|
|
234
234
|
existing.a2aInstanceId = req.a2a_instance_id;
|
|
235
|
+
delete existing.a2aProtocol;
|
|
235
236
|
existing.version = req.version;
|
|
236
237
|
existing.specVersion = req.spec_version;
|
|
237
238
|
existing.transportEndpoints = req.transport_endpoints;
|
|
@@ -614,6 +615,24 @@ export class ExecutorRegistry extends EventEmitter {
|
|
|
614
615
|
getRegistration(executorId) {
|
|
615
616
|
return this.executors.get(executorId);
|
|
616
617
|
}
|
|
618
|
+
/** Publish the negotiated interface used for the most recent A2A dispatch. */
|
|
619
|
+
recordA2AProtocolSelection(executorId, selection) {
|
|
620
|
+
const executor = this.executors.get(executorId);
|
|
621
|
+
if (!executor)
|
|
622
|
+
return false;
|
|
623
|
+
executor.a2aProtocol = {
|
|
624
|
+
...selection,
|
|
625
|
+
selectedAt: selection.selectedAt ?? new Date().toISOString(),
|
|
626
|
+
};
|
|
627
|
+
this.emit('executor:a2a_protocol_selected', {
|
|
628
|
+
executorId,
|
|
629
|
+
selectedVersion: selection.selectedVersion,
|
|
630
|
+
protocolBinding: selection.interface.protocolBinding,
|
|
631
|
+
interfaceUrl: selection.interface.url,
|
|
632
|
+
...(selection.fallbackReason ? { fallbackReason: selection.fallbackReason } : {}),
|
|
633
|
+
});
|
|
634
|
+
return true;
|
|
635
|
+
}
|
|
617
636
|
/**
|
|
618
637
|
* Pick the best executor matching the given filter.
|
|
619
638
|
*
|
|
@@ -708,6 +727,16 @@ function toSummary(e) {
|
|
|
708
727
|
};
|
|
709
728
|
if (e.a2aInstanceId)
|
|
710
729
|
summary.a2a_instance_id = e.a2aInstanceId;
|
|
730
|
+
if (e.a2aProtocol) {
|
|
731
|
+
summary.a2a_protocol = {
|
|
732
|
+
policy: e.a2aProtocol.policy,
|
|
733
|
+
selected_version: e.a2aProtocol.selectedVersion,
|
|
734
|
+
protocol_binding: e.a2aProtocol.interface.protocolBinding,
|
|
735
|
+
interface_url: e.a2aProtocol.interface.url,
|
|
736
|
+
selected_at: e.a2aProtocol.selectedAt,
|
|
737
|
+
...(e.a2aProtocol.fallbackReason ? { fallback_reason: e.a2aProtocol.fallbackReason } : {}),
|
|
738
|
+
};
|
|
739
|
+
}
|
|
711
740
|
return summary;
|
|
712
741
|
}
|
|
713
742
|
// Singleton instance
|
|
@@ -46,6 +46,9 @@ export class MissionConductor {
|
|
|
46
46
|
* crash-resilient resume path; their prior results are carried forward.
|
|
47
47
|
*/
|
|
48
48
|
async conduct(plan, pool, resumeFrom) {
|
|
49
|
+
if (plan.cycles.some((cycle) => cycle.graph) && !plan.graph) {
|
|
50
|
+
throw new Error('Graph-projected worker cycles require MissionPlan.graph identity.');
|
|
51
|
+
}
|
|
49
52
|
const carried = new Map();
|
|
50
53
|
if (resumeFrom) {
|
|
51
54
|
for (const c of resumeFrom.cycles) {
|
|
@@ -62,10 +65,17 @@ export class MissionConductor {
|
|
|
62
65
|
totalCost: 0,
|
|
63
66
|
checkpoint: { completed: [], pending: [], failed: [] },
|
|
64
67
|
runtimesUsed: [],
|
|
68
|
+
...(plan.graph ? { graph: structuredClone(plan.graph) } : {}),
|
|
65
69
|
};
|
|
66
70
|
ledger.activityLog.push(`mission ${plan.missionId} start — goal: ${plan.goal} — ${plan.cycles.length} cycle(s)` +
|
|
67
71
|
(resumeFrom ? ` (resume: ${carried.size} carried)` : ''));
|
|
68
72
|
for (const cycle of plan.cycles) {
|
|
73
|
+
const graph = plan.graph && cycle.graph ? {
|
|
74
|
+
...structuredClone(plan.graph),
|
|
75
|
+
...structuredClone(cycle.graph),
|
|
76
|
+
schemaVersion: 'graph.flow.aiwg.io/v1',
|
|
77
|
+
nodeRunId: cycle.graph.nodeRunId ?? `${plan.graph.runId}:${cycle.graph.nodeId}`,
|
|
78
|
+
} : undefined;
|
|
69
79
|
// Resume: carry a previously-completed cycle forward, identical bookkeeping.
|
|
70
80
|
const prior = carried.get(cycle.id);
|
|
71
81
|
if (prior) {
|
|
@@ -86,6 +96,7 @@ export class MissionConductor {
|
|
|
86
96
|
routed: false,
|
|
87
97
|
reason: `no stack adapter registered for runtime '${cycle.runtime}'`,
|
|
88
98
|
cost: 0,
|
|
99
|
+
...(graph ? { graph } : {}),
|
|
89
100
|
};
|
|
90
101
|
ledger.cycles.push(result);
|
|
91
102
|
ledger.checkpoint.failed.push(cycle.id);
|
|
@@ -104,6 +115,7 @@ export class MissionConductor {
|
|
|
104
115
|
routed: false,
|
|
105
116
|
reason: `no connected executor advertises ${filter.capabilities.join(', ')}`,
|
|
106
117
|
cost: 0,
|
|
118
|
+
...(graph ? { graph } : {}),
|
|
107
119
|
};
|
|
108
120
|
ledger.cycles.push(result);
|
|
109
121
|
ledger.checkpoint.failed.push(cycle.id);
|
|
@@ -111,7 +123,7 @@ export class MissionConductor {
|
|
|
111
123
|
continue;
|
|
112
124
|
}
|
|
113
125
|
const executor = routing.selected.executor;
|
|
114
|
-
const invocation = adapter.invoke(cycle.prompt);
|
|
126
|
+
const invocation = adapter.invoke(cycle.prompt, graph);
|
|
115
127
|
ledger.activityLog.push(`cycle ${cycle.id} → executor ${executor.name} (${executor.executorId}) on ${cycle.runtime}: ${invocation.describe}`);
|
|
116
128
|
let output;
|
|
117
129
|
let cost = 0;
|
|
@@ -129,6 +141,7 @@ export class MissionConductor {
|
|
|
129
141
|
routed: true,
|
|
130
142
|
reason: `worker error: ${err instanceof Error ? err.message : String(err)}`,
|
|
131
143
|
cost: 0,
|
|
144
|
+
...(graph ? { graph } : {}),
|
|
132
145
|
};
|
|
133
146
|
ledger.cycles.push(result);
|
|
134
147
|
ledger.checkpoint.failed.push(cycle.id);
|
|
@@ -144,6 +157,7 @@ export class MissionConductor {
|
|
|
144
157
|
reason: routing.selected.matchReason,
|
|
145
158
|
output,
|
|
146
159
|
cost,
|
|
160
|
+
...(graph ? { graph } : {}),
|
|
147
161
|
};
|
|
148
162
|
ledger.cycles.push(result);
|
|
149
163
|
ledger.checkpoint.completed.push(cycle.id);
|
|
@@ -23,7 +23,7 @@ function makeAdapter(runtime, primitive) {
|
|
|
23
23
|
runtime,
|
|
24
24
|
runtimeCapability,
|
|
25
25
|
primitive,
|
|
26
|
-
invoke(prompt) {
|
|
26
|
+
invoke(prompt, graph) {
|
|
27
27
|
// prompt is carried by the dispatch payload; the descriptor records the
|
|
28
28
|
// mechanism so the conductor's ledger is identical-shape across stacks.
|
|
29
29
|
const trimmed = prompt.length > 60 ? `${prompt.slice(0, 57)}...` : prompt;
|
|
@@ -31,6 +31,7 @@ function makeAdapter(runtime, primitive) {
|
|
|
31
31
|
runtimeCapability,
|
|
32
32
|
primitive,
|
|
33
33
|
describe: `dispatch worker to ${runtime} executor via ${primitive} (${trimmed})`,
|
|
34
|
+
...(graph ? { graph: structuredClone(graph) } : {}),
|
|
34
35
|
};
|
|
35
36
|
},
|
|
36
37
|
};
|
|
@@ -130,7 +130,10 @@ export const telemetryStore = new TelemetryStore();
|
|
|
130
130
|
// Event Factory
|
|
131
131
|
// ============================================================
|
|
132
132
|
let eventCounter = 0;
|
|
133
|
-
export function createEvent(type, sessionId, payload, missionId) {
|
|
133
|
+
export function createEvent(type, sessionId, payload, missionId, graph) {
|
|
134
|
+
if (type.startsWith('graph.') && !graph) {
|
|
135
|
+
throw new Error(`Telemetry event '${type}' requires graph execution metadata.`);
|
|
136
|
+
}
|
|
134
137
|
return {
|
|
135
138
|
id: `evt-${Date.now()}-${++eventCounter}`,
|
|
136
139
|
sessionId,
|
|
@@ -138,6 +141,7 @@ export function createEvent(type, sessionId, payload, missionId) {
|
|
|
138
141
|
timestamp: new Date().toISOString(),
|
|
139
142
|
type,
|
|
140
143
|
payload,
|
|
144
|
+
...(graph ? { graph: structuredClone(graph) } : {}),
|
|
141
145
|
};
|
|
142
146
|
}
|
|
143
147
|
//# sourceMappingURL=telemetry.js.map
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import * as fs from 'fs/promises';
|
|
23
23
|
import * as path from 'path';
|
|
24
24
|
import { buildProviderBootstrapBlock, PROVIDER_BOOTSTRAP_START, PROVIDER_BOOTSTRAP_END, } from './workspace-context.js';
|
|
25
|
+
import { dominantLineEnding, withLineEnding } from './line-endings.js';
|
|
25
26
|
export const CLAUDE_HOOK_START = '<!-- AIWG:claude-md-hook:start -->';
|
|
26
27
|
export const CLAUDE_HOOK_END = '<!-- AIWG:claude-md-hook:end -->';
|
|
27
28
|
function buildClaudeArtifactOutputPolicy(policy = {}) {
|
|
@@ -77,7 +78,7 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
|
|
|
77
78
|
catch {
|
|
78
79
|
// Missing/legacy/temporarily malformed config receives the safe default.
|
|
79
80
|
}
|
|
80
|
-
|
|
81
|
+
let block = buildClaudeHookBlock(policy);
|
|
81
82
|
// Case 1: CLAUDE.md does not exist — create a minimal one with just the block.
|
|
82
83
|
let existing;
|
|
83
84
|
try {
|
|
@@ -92,6 +93,8 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
|
|
|
92
93
|
}
|
|
93
94
|
throw err;
|
|
94
95
|
}
|
|
96
|
+
const lineEnding = dominantLineEnding(existing);
|
|
97
|
+
block = withLineEnding(block, lineEnding);
|
|
95
98
|
const startIdx = existing.indexOf(CLAUDE_HOOK_START);
|
|
96
99
|
const endIdx = existing.indexOf(CLAUDE_HOOK_END);
|
|
97
100
|
// Case 2: marker block does not exist — append the block to end of file.
|
|
@@ -117,8 +120,8 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
|
|
|
117
120
|
return result;
|
|
118
121
|
}
|
|
119
122
|
// Ensure the file ends with a single newline before appending.
|
|
120
|
-
const trimmed = existing.replace(
|
|
121
|
-
const updated = `${trimmed}
|
|
123
|
+
const trimmed = existing.replace(/(?:\r?\n)+$/, lineEnding);
|
|
124
|
+
const updated = `${trimmed}${lineEnding}${block}${lineEnding}`;
|
|
122
125
|
await fs.writeFile(claudeMdPath, updated, 'utf8');
|
|
123
126
|
result.action = 'inserted';
|
|
124
127
|
return result;
|
|
@@ -135,8 +138,8 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
|
|
|
135
138
|
await fs.writeFile(backupPath, existing, 'utf8');
|
|
136
139
|
result.backupPath = backupPath;
|
|
137
140
|
}
|
|
138
|
-
const trimmed = existing.replace(
|
|
139
|
-
const updated = `${trimmed}
|
|
141
|
+
const trimmed = existing.replace(/(?:\r?\n)+$/, lineEnding);
|
|
142
|
+
const updated = `${trimmed}${lineEnding}${block}${lineEnding}`;
|
|
140
143
|
await fs.writeFile(claudeMdPath, updated, 'utf8');
|
|
141
144
|
result.action = 'inserted';
|
|
142
145
|
return result;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Select the majority line ending, preferring LF for ties and new files. */
|
|
2
|
+
export function dominantLineEnding(content) {
|
|
3
|
+
const crlfCount = content.match(/\r\n/g)?.length ?? 0;
|
|
4
|
+
const newlineCount = content.match(/\n/g)?.length ?? 0;
|
|
5
|
+
const bareLfCount = newlineCount - crlfCount;
|
|
6
|
+
return crlfCount > bareLfCount ? '\r\n' : '\n';
|
|
7
|
+
}
|
|
8
|
+
/** Render generated text using the line-ending convention of existing content. */
|
|
9
|
+
export function withLineEnding(content, lineEnding) {
|
|
10
|
+
return content.replace(/\r?\n/g, lineEnding);
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=line-endings.js.map
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import * as fs from 'fs/promises';
|
|
15
15
|
import * as path from 'path';
|
|
16
16
|
import { buildProviderBootstrapBlock } from './workspace-context.js';
|
|
17
|
+
import { dominantLineEnding, withLineEnding } from './line-endings.js';
|
|
17
18
|
export const CONTEXT_HOOK_START = '<!-- AIWG:context-hook:start -->';
|
|
18
19
|
export const CONTEXT_HOOK_END = '<!-- AIWG:context-hook:end -->';
|
|
19
20
|
/** The managed block — loads canonical workspace context before framework context. */
|
|
@@ -42,7 +43,7 @@ export function hasContextHook(content) {
|
|
|
42
43
|
*/
|
|
43
44
|
export async function ensureManagedHook(filePath, opts = {}) {
|
|
44
45
|
const base = path.basename(filePath);
|
|
45
|
-
|
|
46
|
+
let block = buildContextHookBlock(opts.provider);
|
|
46
47
|
const result = { path: filePath, action: 'skipped', warnings: [] };
|
|
47
48
|
let existing;
|
|
48
49
|
try {
|
|
@@ -56,6 +57,8 @@ export async function ensureManagedHook(filePath, opts = {}) {
|
|
|
56
57
|
}
|
|
57
58
|
throw err;
|
|
58
59
|
}
|
|
60
|
+
const lineEnding = dominantLineEnding(existing);
|
|
61
|
+
block = withLineEnding(block, lineEnding);
|
|
59
62
|
// Already has both bare includes (operator wired them by hand) — nothing to do.
|
|
60
63
|
if (!existing.includes(CONTEXT_HOOK_START) && /^[ \t]*@WORKSPACE\.md[ \t]*$/m.test(existing) && /^[ \t]*@AIWG\.md[ \t]*$/m.test(existing)) {
|
|
61
64
|
result.action = 'unchanged';
|
|
@@ -65,16 +68,16 @@ export async function ensureManagedHook(filePath, opts = {}) {
|
|
|
65
68
|
const e = existing.indexOf(CONTEXT_HOOK_END);
|
|
66
69
|
// No managed block — append it to the end, preserving everything above.
|
|
67
70
|
if (s === -1 && e === -1) {
|
|
68
|
-
const trimmed = existing.replace(
|
|
69
|
-
await fs.writeFile(filePath, `${trimmed}
|
|
71
|
+
const trimmed = existing.replace(/(?:\r?\n)+$/, lineEnding);
|
|
72
|
+
await fs.writeFile(filePath, `${trimmed}${lineEnding}${block}${lineEnding}`, 'utf8');
|
|
70
73
|
result.action = 'inserted';
|
|
71
74
|
return result;
|
|
72
75
|
}
|
|
73
76
|
// Malformed (one marker only) — repair only with --force to avoid clobbering.
|
|
74
77
|
if (s === -1 || e === -1) {
|
|
75
78
|
if (opts.force) {
|
|
76
|
-
const trimmed = existing.replace(
|
|
77
|
-
await fs.writeFile(filePath, `${trimmed}
|
|
79
|
+
const trimmed = existing.replace(/(?:\r?\n)+$/, lineEnding);
|
|
80
|
+
await fs.writeFile(filePath, `${trimmed}${lineEnding}${block}${lineEnding}`, 'utf8');
|
|
78
81
|
result.action = 'inserted';
|
|
79
82
|
return result;
|
|
80
83
|
}
|