@link-assistant/agent 0.25.8 → 0.26.0
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/README.md +24 -0
- package/package.json +1 -1
- package/src/cli/continuous-mode.js +10 -4
- package/src/cli/model-config.js +37 -0
- package/src/cli/model-resolution.ts +108 -0
- package/src/cli/output.ts +3 -1
- package/src/config/defaults.ts +19 -0
- package/src/index.js +10 -4
- package/src/json-standard/index.ts +16 -3
package/README.md
CHANGED
|
@@ -376,6 +376,30 @@ echo "hi" | agent --json-standard claude
|
|
|
376
376
|
- **Event Types**: `init`, `message`, `tool_use`, `tool_result`, `result`
|
|
377
377
|
- **Timestamps**: ISO 8601 strings
|
|
378
378
|
- **Session ID**: `session_id` (snake_case)
|
|
379
|
+
- **Model**: the `init` event reports the resolved model as `"model": "<provider>/<model>"`
|
|
380
|
+
|
|
381
|
+
#### Model Routing Attestation
|
|
382
|
+
|
|
383
|
+
Both standards emit exactly one `model_resolved` event per run, immediately after
|
|
384
|
+
the model is resolved and before the first request reaches a provider:
|
|
385
|
+
|
|
386
|
+
```json
|
|
387
|
+
{
|
|
388
|
+
"type": "model_resolved",
|
|
389
|
+
"timestamp": "2026-08-11T00:12:31.320Z",
|
|
390
|
+
"requested": "formalai/formal-ai",
|
|
391
|
+
"selector": "formalai/formal-ai",
|
|
392
|
+
"providerID": "formalai",
|
|
393
|
+
"modelID": "formal-ai",
|
|
394
|
+
"source": "cli",
|
|
395
|
+
"matchesRequest": true
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Switch on `type` rather than matching the human-readable
|
|
400
|
+
`using explicit provider/model` log message — the log message is prose and may be
|
|
401
|
+
reworded, the event type is a contract. See
|
|
402
|
+
[docs/model-resolved-event.md](../docs/model-resolved-event.md).
|
|
379
403
|
|
|
380
404
|
### Input Formats
|
|
381
405
|
|
package/package.json
CHANGED
|
@@ -303,8 +303,11 @@ export async function runContinuousServerMode(
|
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
-
// Create event handler for the selected JSON standard
|
|
307
|
-
|
|
306
|
+
// Create event handler for the selected JSON standard.
|
|
307
|
+
// The resolved model rides along so the Claude `init` event reports it (#295).
|
|
308
|
+
const eventHandler = createEventHandler(jsonStandard, sessionID, {
|
|
309
|
+
model: `${providerID}/${modelID}`,
|
|
310
|
+
});
|
|
308
311
|
|
|
309
312
|
// Track if we're currently processing a message
|
|
310
313
|
let isProcessing = false;
|
|
@@ -526,8 +529,11 @@ export async function runContinuousDirectMode(
|
|
|
526
529
|
sessionID = session.id;
|
|
527
530
|
}
|
|
528
531
|
|
|
529
|
-
// Create event handler for the selected JSON standard
|
|
530
|
-
|
|
532
|
+
// Create event handler for the selected JSON standard.
|
|
533
|
+
// The resolved model rides along so the Claude `init` event reports it (#295).
|
|
534
|
+
const eventHandler = createEventHandler(jsonStandard, sessionID, {
|
|
535
|
+
model: `${providerID}/${modelID}`,
|
|
536
|
+
});
|
|
531
537
|
|
|
532
538
|
// Track if we're currently processing a message
|
|
533
539
|
let isProcessing = false;
|
package/src/cli/model-config.js
CHANGED
|
@@ -10,10 +10,12 @@ import {
|
|
|
10
10
|
DEFAULT_MODEL_ID,
|
|
11
11
|
getDefaultModel,
|
|
12
12
|
getDefaultModelParts,
|
|
13
|
+
getDefaultModelSource,
|
|
13
14
|
getDefaultCompactionModel,
|
|
14
15
|
getDefaultCompactionModels,
|
|
15
16
|
getDefaultCompactionSafetyMarginPercent,
|
|
16
17
|
} from './defaults.ts';
|
|
18
|
+
import { outputModelResolved } from './model-resolution.ts';
|
|
17
19
|
|
|
18
20
|
export class ModelResolutionError extends Error {
|
|
19
21
|
constructor({ modelArgv, defaultModel }) {
|
|
@@ -96,6 +98,19 @@ export async function parseModelConfig(
|
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
// Attestation inputs (#295): record what was requested, and where it came
|
|
102
|
+
// from, before resolution can rewrite the effective model. `argv.model` is
|
|
103
|
+
// only treated as a request when it differs from the default, because yargs
|
|
104
|
+
// fills it with the default value even when no --model flag was passed.
|
|
105
|
+
const argvModelArg =
|
|
106
|
+
typeof argv.model === 'string' && argv.model !== defaultModel
|
|
107
|
+
? argv.model
|
|
108
|
+
: null;
|
|
109
|
+
const requestedModel = cliModelArg ?? argvModelArg;
|
|
110
|
+
const modelSource = requestedModel
|
|
111
|
+
? 'cli'
|
|
112
|
+
: getDefaultModelSource(defaultOptions);
|
|
113
|
+
|
|
99
114
|
let providerID;
|
|
100
115
|
let modelID;
|
|
101
116
|
|
|
@@ -257,6 +272,28 @@ export async function parseModelConfig(
|
|
|
257
272
|
}
|
|
258
273
|
}
|
|
259
274
|
|
|
275
|
+
// Machine-readable routing attestation (#295).
|
|
276
|
+
// Emitted last inside this function, which is still the earliest point where
|
|
277
|
+
// the effective model is final: --use-existing-claude-oauth above is the one
|
|
278
|
+
// path that can replace an already selected model, and the attestation must
|
|
279
|
+
// describe what actually runs. It is emitted before session creation and
|
|
280
|
+
// before any completion request, so a consumer can terminate the run before a
|
|
281
|
+
// request reaches a provider it did not ask for. Claude NDJSON streams must
|
|
282
|
+
// stay one line, so that standard forces compact output regardless of
|
|
283
|
+
// --compact-json.
|
|
284
|
+
const usesClaudeStandard =
|
|
285
|
+
argv['json-standard'] === 'claude' || argv.jsonStandard === 'claude';
|
|
286
|
+
outputModelResolved(
|
|
287
|
+
{
|
|
288
|
+
requested: requestedModel,
|
|
289
|
+
selector: modelArg,
|
|
290
|
+
providerID,
|
|
291
|
+
modelID,
|
|
292
|
+
source: modelSource,
|
|
293
|
+
},
|
|
294
|
+
usesClaudeStandard || argv['compact-json'] === true ? true : undefined
|
|
295
|
+
);
|
|
296
|
+
|
|
260
297
|
return { providerID, modelID, compactionModel: compactionModelResult };
|
|
261
298
|
}
|
|
262
299
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Machine-readable model routing attestation.
|
|
3
|
+
*
|
|
4
|
+
* The CLI already logs which provider/model it settled on, but that record is
|
|
5
|
+
* discriminated by a free-text `message` ("using explicit provider/model").
|
|
6
|
+
* A consumer that must *verify* routing — for example a supervisor that kills
|
|
7
|
+
* the run when the agent resolves anything other than the requested model —
|
|
8
|
+
* would have to match English prose, so an ordinary reword upstream silently
|
|
9
|
+
* disables a downstream safety check.
|
|
10
|
+
*
|
|
11
|
+
* `model_resolved` is the contract instead: one event per run, emitted on
|
|
12
|
+
* every resolution path (explicit `provider/model`, bare model id and default),
|
|
13
|
+
* before the first `step_start` and before any completion request.
|
|
14
|
+
*
|
|
15
|
+
* @see https://github.com/link-assistant/agent/issues/295
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { output } from './output';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Stable `type` of the routing attestation event.
|
|
22
|
+
*
|
|
23
|
+
* This string is a machine contract: consumers switch on it. Never reword it —
|
|
24
|
+
* a change here is a breaking change, unlike a change to any log message.
|
|
25
|
+
*/
|
|
26
|
+
export const MODEL_RESOLVED_EVENT_TYPE = 'model_resolved';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Where the applied model selector came from.
|
|
30
|
+
* - `cli`: the `--model`/`-m` command line argument.
|
|
31
|
+
* - `config`: no CLI flag; an operator-configured default is in effect.
|
|
32
|
+
* - `default`: no CLI flag and no override; the built-in default model.
|
|
33
|
+
*/
|
|
34
|
+
export type ModelSource = 'cli' | 'config' | 'default';
|
|
35
|
+
|
|
36
|
+
export interface ModelResolvedDetails {
|
|
37
|
+
/** Raw `--model` value, or `null` when no model was requested. */
|
|
38
|
+
requested: string | null;
|
|
39
|
+
/** Selector actually applied: `requested`, or the effective default. */
|
|
40
|
+
selector: string;
|
|
41
|
+
providerID: string;
|
|
42
|
+
modelID: string;
|
|
43
|
+
source: ModelSource;
|
|
44
|
+
/** ISO timestamp; injectable so tests stay deterministic. */
|
|
45
|
+
timestamp?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ModelResolvedEvent extends ModelResolvedDetails {
|
|
49
|
+
type: typeof MODEL_RESOLVED_EVENT_TYPE;
|
|
50
|
+
timestamp: string;
|
|
51
|
+
/**
|
|
52
|
+
* Whether the resolved provider/model satisfies `selector`. `false` means the
|
|
53
|
+
* run is about to talk to a model other than the one that was selected, even
|
|
54
|
+
* when the selector came from a default rather than from `--model`.
|
|
55
|
+
*/
|
|
56
|
+
matchesRequest: boolean;
|
|
57
|
+
[key: string]: unknown;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Does `providerID`/`modelID` satisfy the selector the caller supplied?
|
|
62
|
+
*
|
|
63
|
+
* A selector with a `/` names both provider and model and must match both.
|
|
64
|
+
* A bare selector names only the model, so the resolved provider is free.
|
|
65
|
+
*/
|
|
66
|
+
export function selectorMatchesModel(
|
|
67
|
+
selector: string,
|
|
68
|
+
providerID: string,
|
|
69
|
+
modelID: string
|
|
70
|
+
): boolean {
|
|
71
|
+
if (selector.includes('/')) {
|
|
72
|
+
return selector === `${providerID}/${modelID}`;
|
|
73
|
+
}
|
|
74
|
+
return selector === modelID;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Build the attestation event without emitting it (pure; used by tests). */
|
|
78
|
+
export function buildModelResolvedEvent(
|
|
79
|
+
details: ModelResolvedDetails
|
|
80
|
+
): ModelResolvedEvent {
|
|
81
|
+
const { requested, selector, providerID, modelID, source, timestamp } =
|
|
82
|
+
details;
|
|
83
|
+
return {
|
|
84
|
+
type: MODEL_RESOLVED_EVENT_TYPE,
|
|
85
|
+
timestamp: timestamp ?? new Date().toISOString(),
|
|
86
|
+
requested: requested ?? null,
|
|
87
|
+
selector,
|
|
88
|
+
providerID,
|
|
89
|
+
modelID,
|
|
90
|
+
source,
|
|
91
|
+
matchesRequest: selectorMatchesModel(selector, providerID, modelID),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Emit the attestation on stdout and return the emitted event.
|
|
97
|
+
*
|
|
98
|
+
* @param compact - Force single-line JSON (required for NDJSON streams such as
|
|
99
|
+
* `--output-format stream-json`); defaults to the global compact setting.
|
|
100
|
+
*/
|
|
101
|
+
export function outputModelResolved(
|
|
102
|
+
details: ModelResolvedDetails,
|
|
103
|
+
compact?: boolean
|
|
104
|
+
): ModelResolvedEvent {
|
|
105
|
+
const event = buildModelResolvedEvent(details);
|
|
106
|
+
output(event, compact);
|
|
107
|
+
return event;
|
|
108
|
+
}
|
package/src/cli/output.ts
CHANGED
|
@@ -28,7 +28,9 @@ export type OutputType =
|
|
|
28
28
|
| 'init'
|
|
29
29
|
| 'message'
|
|
30
30
|
| 'tool_result'
|
|
31
|
-
| 'input'
|
|
31
|
+
| 'input'
|
|
32
|
+
// Routing attestation emitted once per run, see cli/model-resolution.ts (#295)
|
|
33
|
+
| 'model_resolved';
|
|
32
34
|
|
|
33
35
|
/**
|
|
34
36
|
* Base interface for all output messages
|
package/src/config/defaults.ts
CHANGED
|
@@ -112,6 +112,25 @@ export function getDefaultModel(options: DefaultConfigOptions = {}): string {
|
|
|
112
112
|
);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Report where the effective default model came from.
|
|
117
|
+
*
|
|
118
|
+
* `'config'` means an operator override is in effect (`defaultOptions.defaultModel`
|
|
119
|
+
* or `LINK_ASSISTANT_AGENT_DEFAULT_MODEL`); `'default'` means the built-in
|
|
120
|
+
* `DEFAULT_MODEL`. Used by the `model_resolved` attestation so a consumer can
|
|
121
|
+
* tell a configured fallback from the shipped one.
|
|
122
|
+
*
|
|
123
|
+
* @see https://github.com/link-assistant/agent/issues/295
|
|
124
|
+
*/
|
|
125
|
+
export function getDefaultModelSource(
|
|
126
|
+
options: DefaultConfigOptions = {}
|
|
127
|
+
): 'config' | 'default' {
|
|
128
|
+
const env = options.env ?? process.env;
|
|
129
|
+
if (optionString(options.defaultModel)) return 'config';
|
|
130
|
+
if (envString(env, DEFAULT_MODEL_ENV)) return 'config';
|
|
131
|
+
return 'default';
|
|
132
|
+
}
|
|
133
|
+
|
|
115
134
|
export function getDefaultCompactionModel(
|
|
116
135
|
options: DefaultConfigOptions = {}
|
|
117
136
|
): string {
|
package/src/index.js
CHANGED
|
@@ -486,8 +486,11 @@ async function runServerMode(
|
|
|
486
486
|
}
|
|
487
487
|
}
|
|
488
488
|
|
|
489
|
-
// Create event handler for the selected JSON standard
|
|
490
|
-
|
|
489
|
+
// Create event handler for the selected JSON standard.
|
|
490
|
+
// The resolved model rides along so the Claude `init` event reports it (#295).
|
|
491
|
+
const eventHandler = createEventHandler(jsonStandard, sessionID, {
|
|
492
|
+
model: `${providerID}/${modelID}`,
|
|
493
|
+
});
|
|
491
494
|
|
|
492
495
|
// Subscribe to all bus events and output in selected format
|
|
493
496
|
const { unsub: eventUnsub, idlePromise: eventPromise } =
|
|
@@ -580,8 +583,11 @@ async function runDirectMode(
|
|
|
580
583
|
sessionID = session.id;
|
|
581
584
|
}
|
|
582
585
|
|
|
583
|
-
// Create event handler for the selected JSON standard
|
|
584
|
-
|
|
586
|
+
// Create event handler for the selected JSON standard.
|
|
587
|
+
// The resolved model rides along so the Claude `init` event reports it (#295).
|
|
588
|
+
const eventHandler = createEventHandler(jsonStandard, sessionID, {
|
|
589
|
+
model: `${providerID}/${modelID}`,
|
|
590
|
+
});
|
|
585
591
|
|
|
586
592
|
// Subscribe to all bus events and output in selected format
|
|
587
593
|
const { unsub: eventUnsub, idlePromise: eventPromise } =
|
|
@@ -75,10 +75,14 @@ export function serializeOutput(
|
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
77
|
* Convert OpenCode event to Claude event format
|
|
78
|
+
*
|
|
79
|
+
* @param model - Resolved `providerID/modelID`, reported on the `init` event so
|
|
80
|
+
* Claude-standard consumers see which model the run settled on (#295)
|
|
78
81
|
*/
|
|
79
82
|
export function convertOpenCodeToClaude(
|
|
80
83
|
event: OpenCodeEvent,
|
|
81
|
-
startTime: number
|
|
84
|
+
startTime: number,
|
|
85
|
+
model?: string
|
|
82
86
|
): ClaudeEvent | null {
|
|
83
87
|
const timestamp = new Date(event.timestamp).toISOString();
|
|
84
88
|
const session_id = event.sessionID;
|
|
@@ -89,6 +93,7 @@ export function convertOpenCodeToClaude(
|
|
|
89
93
|
type: 'init',
|
|
90
94
|
timestamp,
|
|
91
95
|
session_id,
|
|
96
|
+
...(model ? { model } : {}),
|
|
92
97
|
};
|
|
93
98
|
|
|
94
99
|
case 'text':
|
|
@@ -157,9 +162,17 @@ export function convertOpenCodeToClaude(
|
|
|
157
162
|
|
|
158
163
|
/**
|
|
159
164
|
* Create an event output handler based on the selected standard
|
|
165
|
+
*
|
|
166
|
+
* @param options.model - Resolved `providerID/modelID` for the run; carried on
|
|
167
|
+
* the Claude `init` event (#295)
|
|
160
168
|
*/
|
|
161
|
-
export function createEventHandler(
|
|
169
|
+
export function createEventHandler(
|
|
170
|
+
standard: JsonStandard,
|
|
171
|
+
sessionID: string,
|
|
172
|
+
options: { model?: string } = {}
|
|
173
|
+
) {
|
|
162
174
|
const startTime = Date.now();
|
|
175
|
+
const { model } = options;
|
|
163
176
|
|
|
164
177
|
return {
|
|
165
178
|
/**
|
|
@@ -169,7 +182,7 @@ export function createEventHandler(standard: JsonStandard, sessionID: string) {
|
|
|
169
182
|
const outputStream =
|
|
170
183
|
event.type === 'error' ? process.stderr : process.stdout;
|
|
171
184
|
if (standard === 'claude') {
|
|
172
|
-
const claudeEvent = convertOpenCodeToClaude(event, startTime);
|
|
185
|
+
const claudeEvent = convertOpenCodeToClaude(event, startTime, model);
|
|
173
186
|
if (claudeEvent) {
|
|
174
187
|
outputStream.write(serializeOutput(claudeEvent, standard));
|
|
175
188
|
}
|