@aiwg/cli 2026.8.17 → 2026.8.18
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/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 +4 -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/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.mjs +243 -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/update/checker.mjs +16 -15
- package/dist/src/update/notifier.mjs +8 -3
- package/dist/src/update/service.mjs +49 -5
- package/package.json +1 -1
|
@@ -0,0 +1,243 @@
|
|
|
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
|
+
|
|
16
|
+
export const INSTALLATION_IDENTITY_VERSION = 1;
|
|
17
|
+
export const INSTALLATION_FILE = 'installation.json';
|
|
18
|
+
const METHODS = new Set(['npm', 'web', 'source']);
|
|
19
|
+
const RUN_MODES = new Set(['normal', 'development']);
|
|
20
|
+
const CHANNELS = new Set(['stable', 'next', 'nightly', 'edge']);
|
|
21
|
+
const STRATEGIES = new Set(['npm-global', 'signed-web', 'source-git']);
|
|
22
|
+
|
|
23
|
+
function canonicalPath(value) {
|
|
24
|
+
const resolved = path.resolve(value);
|
|
25
|
+
try { return realpathSync.native(resolved); } catch { return resolved; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function packageName(root) {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8')).name ?? null;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function inferInstallationMethod(root) {
|
|
37
|
+
if (packageName(root) === '@aiwg/cli') return 'web';
|
|
38
|
+
if (existsSync(path.join(root, '.git'))) return 'source';
|
|
39
|
+
return 'npm';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveExecutable(name, options = {}) {
|
|
43
|
+
const env = options.env ?? process.env;
|
|
44
|
+
const explicit = options.managerExecutable ?? env.AIWG_PACKAGE_MANAGER_EXECUTABLE;
|
|
45
|
+
if (explicit) return canonicalPath(explicit);
|
|
46
|
+
if (name === 'npm' && env.npm_execpath) return canonicalPath(env.npm_execpath);
|
|
47
|
+
|
|
48
|
+
const besideNode = path.join(path.dirname(process.execPath), process.platform === 'win32' ? `${name}.cmd` : name);
|
|
49
|
+
if (existsSync(besideNode)) return canonicalPath(besideNode);
|
|
50
|
+
try {
|
|
51
|
+
const finder = process.platform === 'win32' ? 'where.exe' : 'which';
|
|
52
|
+
const found = execFileSync(finder, [name], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
|
|
53
|
+
.split(/\r?\n/, 1)[0]?.trim();
|
|
54
|
+
return found ? canonicalPath(found) : null;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function strategyFor(method) {
|
|
61
|
+
if (method === 'web') return 'signed-web';
|
|
62
|
+
if (method === 'source') return 'source-git';
|
|
63
|
+
return 'npm-global';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function executableIsUsable(file) {
|
|
67
|
+
try {
|
|
68
|
+
if (!statSync(file).isFile()) return false;
|
|
69
|
+
if (process.platform !== 'win32') accessSync(file, constants.X_OK);
|
|
70
|
+
return true;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function validateIdentity(value, file) {
|
|
77
|
+
const invalid = !value || typeof value !== 'object'
|
|
78
|
+
|| value.schemaVersion !== INSTALLATION_IDENTITY_VERSION
|
|
79
|
+
|| !METHODS.has(value.method)
|
|
80
|
+
|| !RUN_MODES.has(value.runMode)
|
|
81
|
+
|| !CHANNELS.has(value.channel)
|
|
82
|
+
|| typeof value.root !== 'string'
|
|
83
|
+
|| !path.isAbsolute(value.root)
|
|
84
|
+
|| !STRATEGIES.has(value.updateStrategy)
|
|
85
|
+
|| value.updateStrategy !== strategyFor(value.method)
|
|
86
|
+
|| (value.managerExecutable !== null
|
|
87
|
+
&& (typeof value.managerExecutable !== 'string' || !path.isAbsolute(value.managerExecutable)));
|
|
88
|
+
if (invalid) {
|
|
89
|
+
const error = new Error(`Invalid AIWG installation identity at ${file}. Run \`aiwg installation adopt\` to replace it.`);
|
|
90
|
+
error.code = 'AIWG_INSTALLATION_INVALID';
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function installationFile(options = {}) {
|
|
97
|
+
return path.join(resolveUserConfigDir(options), INSTALLATION_FILE);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function createInstallationIdentity(options) {
|
|
101
|
+
if (!options?.actualRoot) throw new Error('actualRoot is required to create an installation identity');
|
|
102
|
+
const root = canonicalPath(options.root ?? options.actualRoot);
|
|
103
|
+
const method = options.method ?? inferInstallationMethod(root);
|
|
104
|
+
const runMode = options.runMode ?? (method === 'source' ? 'development' : 'normal');
|
|
105
|
+
const requestedChannel = options.channel ?? (runMode === 'development' ? 'edge' : 'stable');
|
|
106
|
+
const channel = requestedChannel === 'latest'
|
|
107
|
+
? 'stable'
|
|
108
|
+
: ['alpha', 'beta', 'rc'].includes(requestedChannel) ? 'next' : requestedChannel;
|
|
109
|
+
const executableName = method === 'npm' ? 'npm' : method === 'source' ? 'git' : null;
|
|
110
|
+
return {
|
|
111
|
+
schemaVersion: INSTALLATION_IDENTITY_VERSION,
|
|
112
|
+
runMode,
|
|
113
|
+
method,
|
|
114
|
+
root,
|
|
115
|
+
updateStrategy: options.updateStrategy ?? strategyFor(method),
|
|
116
|
+
managerExecutable: executableName ? resolveExecutable(executableName, options) : null,
|
|
117
|
+
channel,
|
|
118
|
+
edgePath: options.edgePath ? canonicalPath(options.edgePath) : (runMode === 'development' ? root : null),
|
|
119
|
+
checkOnStartup: options.checkOnStartup ?? true,
|
|
120
|
+
lastUpdateCheck: options.lastUpdateCheck ?? null,
|
|
121
|
+
updateCheckInterval: options.updateCheckInterval ?? 86_400_000,
|
|
122
|
+
recordedAt: options.recordedAt ?? new Date().toISOString(),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function saveInstallationIdentity(identity, options = {}) {
|
|
127
|
+
const file = installationFile(options);
|
|
128
|
+
const validated = validateIdentity(identity, file);
|
|
129
|
+
mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
130
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
131
|
+
writeFileSync(temporary, `${JSON.stringify(validated, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
132
|
+
renameSync(temporary, file);
|
|
133
|
+
return validated;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function readLegacy(options = {}) {
|
|
137
|
+
const file = path.join(resolveUserConfigDir(options), 'channel.json');
|
|
138
|
+
try { return JSON.parse(readFileSync(file, 'utf8')); } catch { return null; }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Load the canonical record, migrating legacy channel.json on first access. */
|
|
142
|
+
export function loadInstallationIdentity(options = {}) {
|
|
143
|
+
const file = installationFile(options);
|
|
144
|
+
if (existsSync(file)) {
|
|
145
|
+
try { return validateIdentity(JSON.parse(readFileSync(file, 'utf8')), file); }
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (error?.code === 'AIWG_INSTALLATION_INVALID') throw error;
|
|
148
|
+
const wrapped = new Error(`Cannot read AIWG installation identity at ${file}: ${error.message}`);
|
|
149
|
+
wrapped.code = 'AIWG_INSTALLATION_INVALID';
|
|
150
|
+
throw wrapped;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (options.createIfMissing === false) return null;
|
|
154
|
+
if (!options.actualRoot) return null;
|
|
155
|
+
|
|
156
|
+
const legacy = options.legacyConfig ?? readLegacy(options) ?? {};
|
|
157
|
+
const development = legacy.devMode === true;
|
|
158
|
+
const root = development && legacy.edgePath ? legacy.edgePath : options.actualRoot;
|
|
159
|
+
const method = options.method ?? (development ? 'source' : inferInstallationMethod(root));
|
|
160
|
+
const identity = createInstallationIdentity({
|
|
161
|
+
...options,
|
|
162
|
+
root,
|
|
163
|
+
method,
|
|
164
|
+
runMode: development ? 'development' : undefined,
|
|
165
|
+
channel: legacy.channel ?? options.channel,
|
|
166
|
+
edgePath: legacy.edgePath,
|
|
167
|
+
lastUpdateCheck: legacy.lastUpdateCheck,
|
|
168
|
+
updateCheckInterval: legacy.updateCheckInterval,
|
|
169
|
+
checkOnStartup: legacy.checkOnStartup,
|
|
170
|
+
});
|
|
171
|
+
return saveInstallationIdentity(identity, options);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function inspectInstallation(options = {}) {
|
|
175
|
+
const actualRoot = canonicalPath(options.actualRoot);
|
|
176
|
+
const actualMethod = options.actualMethod ?? inferInstallationMethod(actualRoot);
|
|
177
|
+
const identity = options.identity ?? loadInstallationIdentity({ ...options, actualRoot });
|
|
178
|
+
if (!identity) return { state: 'unrecorded', identity: null, actualRoot, actualMethod, drift: ['installation identity is not recorded'] };
|
|
179
|
+
|
|
180
|
+
const drift = [];
|
|
181
|
+
const canonicalRoot = canonicalPath(identity.root);
|
|
182
|
+
if (!existsSync(canonicalRoot)) drift.push(`canonical root does not exist: ${canonicalRoot}`);
|
|
183
|
+
if (canonicalRoot !== actualRoot) drift.push(`actual root ${actualRoot} differs from canonical root ${canonicalRoot}`);
|
|
184
|
+
if (identity.method !== actualMethod) drift.push(`actual method ${actualMethod} differs from canonical method ${identity.method}`);
|
|
185
|
+
if (identity.method !== 'web' && !identity.managerExecutable) {
|
|
186
|
+
drift.push(`canonical ${identity.method} installation has no recorded manager executable`);
|
|
187
|
+
}
|
|
188
|
+
if (identity.managerExecutable && !executableIsUsable(identity.managerExecutable)) {
|
|
189
|
+
drift.push(`recorded manager executable is missing or not executable: ${identity.managerExecutable}`);
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
state: drift.length === 0 ? 'aligned' : (existsSync(canonicalRoot) ? 'mismatch' : 'stale'),
|
|
193
|
+
identity,
|
|
194
|
+
canonicalRoot,
|
|
195
|
+
actualRoot,
|
|
196
|
+
actualMethod,
|
|
197
|
+
drift,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function formatInstallationDiagnostic(status) {
|
|
202
|
+
if (status.state === 'aligned') return 'Canonical installation is aligned.';
|
|
203
|
+
return [
|
|
204
|
+
'AIWG installation identity drift detected; update and refresh are blocked.',
|
|
205
|
+
...status.drift.map((item) => `- ${item}`),
|
|
206
|
+
'Inspect: aiwg installation show',
|
|
207
|
+
'Adopt this installation: aiwg installation adopt',
|
|
208
|
+
'Switch deliberately: aiwg installation switch --root <path> --method <npm|web|source> [--manager <absolute-path>]',
|
|
209
|
+
].join('\n');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function assertCanonicalInstallation(options = {}) {
|
|
213
|
+
const status = inspectInstallation(options);
|
|
214
|
+
if (status.state !== 'aligned') {
|
|
215
|
+
const error = new Error(formatInstallationDiagnostic(status));
|
|
216
|
+
error.code = 'AIWG_INSTALLATION_DRIFT';
|
|
217
|
+
error.status = status;
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
return status;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function adoptInstallation(options) {
|
|
224
|
+
const inferred = inferInstallationMethod(options.actualRoot);
|
|
225
|
+
if (options.method && options.method !== inferred) {
|
|
226
|
+
throw new Error(`Cannot adopt ${options.actualRoot} as ${options.method}; package contents identify it as ${inferred}.`);
|
|
227
|
+
}
|
|
228
|
+
const identity = createInstallationIdentity({ ...options, root: options.actualRoot });
|
|
229
|
+
saveInstallationIdentity(identity, options);
|
|
230
|
+
return inspectInstallation({ ...options, actualRoot: identity.root, identity });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function switchInstallation(options) {
|
|
234
|
+
if (!options?.root || !options?.method) throw new Error('switch requires root and method');
|
|
235
|
+
if (!existsSync(path.resolve(options.root))) throw new Error(`Installation root does not exist: ${path.resolve(options.root)}`);
|
|
236
|
+
const inferred = inferInstallationMethod(options.root);
|
|
237
|
+
if (options.method !== inferred) {
|
|
238
|
+
throw new Error(`Cannot switch ${options.root} as ${options.method}; package contents identify it as ${inferred}.`);
|
|
239
|
+
}
|
|
240
|
+
const identity = createInstallationIdentity({ ...options, actualRoot: options.root });
|
|
241
|
+
saveInstallationIdentity(identity, options);
|
|
242
|
+
return inspectInstallation({ ...options, actualRoot: identity.root, identity });
|
|
243
|
+
}
|
|
@@ -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
|
|
@@ -15,6 +15,7 @@ import https from 'https';
|
|
|
15
15
|
import { execSync } from 'child_process';
|
|
16
16
|
import { createInterface } from 'readline';
|
|
17
17
|
import { loadConfig, saveConfig, getChannel, getPackageRoot } from '../channel/manager.mjs';
|
|
18
|
+
import { updateInstallation } from './service.mjs';
|
|
18
19
|
|
|
19
20
|
const NPM_REGISTRY = 'https://registry.npmjs.org/aiwg';
|
|
20
21
|
|
|
@@ -212,13 +213,13 @@ async function checkStableUpdates(config) {
|
|
|
212
213
|
console.log('');
|
|
213
214
|
console.log('Updating aiwg...');
|
|
214
215
|
try {
|
|
215
|
-
|
|
216
|
+
await updateInstallation({ config, channel: 'stable' });
|
|
216
217
|
console.log('Update complete! Please restart your terminal.');
|
|
217
218
|
} catch (error) {
|
|
218
|
-
console.error('Update failed. Run manually:
|
|
219
|
+
console.error('Update failed. Run manually: aiwg update');
|
|
219
220
|
}
|
|
220
221
|
} else {
|
|
221
|
-
console.log('Update skipped. Run `
|
|
222
|
+
console.log('Update skipped. Run `aiwg update` when ready.');
|
|
222
223
|
}
|
|
223
224
|
console.log('');
|
|
224
225
|
}
|
|
@@ -251,13 +252,13 @@ async function checkNextUpdates(config) {
|
|
|
251
252
|
console.log('');
|
|
252
253
|
console.log('Updating aiwg@next...');
|
|
253
254
|
try {
|
|
254
|
-
|
|
255
|
+
await updateInstallation({ config, channel: 'next' });
|
|
255
256
|
console.log('Update complete! Please restart your terminal.');
|
|
256
257
|
} catch {
|
|
257
|
-
console.error('Update failed. Run manually:
|
|
258
|
+
console.error('Update failed. Run manually: aiwg update');
|
|
258
259
|
}
|
|
259
260
|
} else {
|
|
260
|
-
console.log('Update skipped. Run `
|
|
261
|
+
console.log('Update skipped. Run `aiwg update` when ready.');
|
|
261
262
|
}
|
|
262
263
|
console.log('');
|
|
263
264
|
}
|
|
@@ -290,13 +291,13 @@ async function checkNightlyUpdates(config) {
|
|
|
290
291
|
console.log('');
|
|
291
292
|
console.log('Updating aiwg@nightly...');
|
|
292
293
|
try {
|
|
293
|
-
|
|
294
|
+
await updateInstallation({ config, channel: 'nightly' });
|
|
294
295
|
console.log('Update complete! Please restart your terminal.');
|
|
295
296
|
} catch {
|
|
296
|
-
console.error('Update failed. Run manually:
|
|
297
|
+
console.error('Update failed. Run manually: aiwg update');
|
|
297
298
|
}
|
|
298
299
|
} else {
|
|
299
|
-
console.log('Update skipped. Run `
|
|
300
|
+
console.log('Update skipped. Run `aiwg update` when ready.');
|
|
300
301
|
}
|
|
301
302
|
console.log('');
|
|
302
303
|
}
|
|
@@ -348,13 +349,13 @@ export async function forceUpdateCheck() {
|
|
|
348
349
|
console.log('Checking for updates on next channel...');
|
|
349
350
|
const latestVersion = await fetchNpmDistTag('next');
|
|
350
351
|
if (!latestVersion) {
|
|
351
|
-
console.log('Could not check npm registry. Try:
|
|
352
|
+
console.log('Could not check npm registry. Try: aiwg update');
|
|
352
353
|
return;
|
|
353
354
|
}
|
|
354
355
|
if (currentVersion !== latestVersion) {
|
|
355
356
|
console.log(`Update available: ${currentVersion} → ${latestVersion}`);
|
|
356
357
|
console.log('');
|
|
357
|
-
console.log('Run:
|
|
358
|
+
console.log('Run: aiwg update');
|
|
358
359
|
} else {
|
|
359
360
|
console.log(`You are on the latest next release: ${currentVersion}`);
|
|
360
361
|
}
|
|
@@ -362,13 +363,13 @@ export async function forceUpdateCheck() {
|
|
|
362
363
|
console.log('Checking for updates on nightly channel...');
|
|
363
364
|
const latestVersion = await fetchNpmDistTag('nightly');
|
|
364
365
|
if (!latestVersion) {
|
|
365
|
-
console.log('Could not check npm registry. Try:
|
|
366
|
+
console.log('Could not check npm registry. Try: aiwg update');
|
|
366
367
|
return;
|
|
367
368
|
}
|
|
368
369
|
if (currentVersion !== latestVersion) {
|
|
369
370
|
console.log(`Update available: ${currentVersion} → ${latestVersion}`);
|
|
370
371
|
console.log('');
|
|
371
|
-
console.log('Run:
|
|
372
|
+
console.log('Run: aiwg update');
|
|
372
373
|
} else {
|
|
373
374
|
console.log(`You are on the latest nightly snapshot: ${currentVersion}`);
|
|
374
375
|
}
|
|
@@ -381,14 +382,14 @@ export async function forceUpdateCheck() {
|
|
|
381
382
|
const latestVersion = await fetchLatestNpmVersion();
|
|
382
383
|
|
|
383
384
|
if (!latestVersion) {
|
|
384
|
-
console.log('Could not check npm registry. Try:
|
|
385
|
+
console.log('Could not check npm registry. Try: aiwg update');
|
|
385
386
|
return;
|
|
386
387
|
}
|
|
387
388
|
|
|
388
389
|
if (isNewerVersion(currentVersion, latestVersion)) {
|
|
389
390
|
console.log(`Update available: ${currentVersion} → ${latestVersion}`);
|
|
390
391
|
console.log('');
|
|
391
|
-
console.log('Run:
|
|
392
|
+
console.log('Run: aiwg update');
|
|
392
393
|
} else {
|
|
393
394
|
console.log(`You are on the latest version: ${currentVersion}`);
|
|
394
395
|
}
|