@link-assistant/agent 0.26.5 → 0.26.7
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 +3 -0
- package/package.json +67 -61
- package/src/agent/agent.ts +2 -2
- package/src/cli/input-queue.js +2 -1
- package/src/cli/model-config.js +106 -18
- package/src/config/host-config-hint.ts +62 -0
- package/src/index.js +8 -0
- package/src/provider/cache.ts +57 -16
- package/src/provider/echo.ts +37 -18
- package/src/provider/opencode-zen.ts +1 -2
- package/src/provider/provider.ts +49 -18
- package/src/session/compaction.ts +1 -1
- package/src/session/processor.ts +1 -1
- package/src/session/prompt.ts +7 -4
- package/src/session/summary.ts +2 -2
package/README.md
CHANGED
|
@@ -347,6 +347,8 @@ Commands:
|
|
|
347
347
|
|
|
348
348
|
See [docs/stdin-mode.md](../docs/stdin-mode.md) for comprehensive stdin mode documentation.
|
|
349
349
|
|
|
350
|
+
Hosts that embed the CLI supply configuration through `LINK_ASSISTANT_AGENT_CONFIG_DIR`, `LINK_ASSISTANT_AGENT_CONFIG` or `LINK_ASSISTANT_AGENT_CONFIG_CONTENT` — never by relocating `XDG_CONFIG_HOME`, which would also hide `gh`/`git` credentials from the agent's tool calls. See [docs/host-integration.md](../docs/host-integration.md).
|
|
351
|
+
|
|
350
352
|
### JSON Output Standards
|
|
351
353
|
|
|
352
354
|
The agent supports two JSON output format standards via the `--json-standard` option:
|
|
@@ -773,6 +775,7 @@ For full documentation, see the [main README](../README.md) in the repository ro
|
|
|
773
775
|
- [Tools Reference](../TOOLS.md)
|
|
774
776
|
- [Usage Examples](../EXAMPLES.md)
|
|
775
777
|
- [Testing Guide](../TESTING.md)
|
|
778
|
+
- [Host Integration](../docs/host-integration.md) — supplying config via `LINK_ASSISTANT_AGENT_CONFIG_DIR`, `LINK_ASSISTANT_AGENT_CONFIG` and `LINK_ASSISTANT_AGENT_CONFIG_CONTENT`
|
|
776
779
|
|
|
777
780
|
## License
|
|
778
781
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/agent",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.7",
|
|
4
4
|
"description": "A minimal, public domain AI CLI agent compatible with OpenCode's JSON interface. Bun-only runtime.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -50,76 +50,77 @@
|
|
|
50
50
|
"LICENSE"
|
|
51
51
|
],
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@agentclientprotocol/sdk": "^
|
|
54
|
-
"@ai-sdk/amazon-bedrock": "^
|
|
55
|
-
"@ai-sdk/anthropic": "^
|
|
56
|
-
"@ai-sdk/azure": "^
|
|
57
|
-
"@ai-sdk/google": "^
|
|
58
|
-
"@ai-sdk/google-vertex": "^
|
|
59
|
-
"@ai-sdk/groq": "^
|
|
60
|
-
"@ai-sdk/mcp": "^
|
|
61
|
-
"@ai-sdk/mistral": "^
|
|
62
|
-
"@ai-sdk/openai": "^
|
|
63
|
-
"@ai-sdk/openai-compatible": "^
|
|
64
|
-
"@ai-sdk/
|
|
65
|
-
"@
|
|
66
|
-
"@
|
|
67
|
-
"@hono/
|
|
68
|
-
"@
|
|
69
|
-
"@
|
|
53
|
+
"@agentclientprotocol/sdk": "^1.5.1",
|
|
54
|
+
"@ai-sdk/amazon-bedrock": "^5.0.101",
|
|
55
|
+
"@ai-sdk/anthropic": "^4.0.68",
|
|
56
|
+
"@ai-sdk/azure": "^4.0.85",
|
|
57
|
+
"@ai-sdk/google": "^4.0.85",
|
|
58
|
+
"@ai-sdk/google-vertex": "^5.0.98",
|
|
59
|
+
"@ai-sdk/groq": "^4.0.52",
|
|
60
|
+
"@ai-sdk/mcp": "^2.0.62",
|
|
61
|
+
"@ai-sdk/mistral": "^4.0.54",
|
|
62
|
+
"@ai-sdk/openai": "^4.0.81",
|
|
63
|
+
"@ai-sdk/openai-compatible": "^3.0.59",
|
|
64
|
+
"@ai-sdk/provider": "^4.0.19",
|
|
65
|
+
"@ai-sdk/xai": "^5.0.12",
|
|
66
|
+
"@clack/prompts": "^1.8.1",
|
|
67
|
+
"@hono/standard-validator": "^0.4.0",
|
|
68
|
+
"@hono/zod-validator": "^0.9.1",
|
|
69
|
+
"@modelcontextprotocol/sdk": "^1.31.0",
|
|
70
|
+
"@octokit/graphql": "^9.0.5",
|
|
70
71
|
"@octokit/rest": "^22.0.1",
|
|
71
72
|
"@openauthjs/openauth": "^0.4.3",
|
|
72
|
-
"@opencode-ai/sdk": "^1.
|
|
73
|
-
"@openrouter/ai-sdk-provider": "^
|
|
74
|
-
"@opentui/core": "0.
|
|
75
|
-
"@opentui/solid": "0.
|
|
76
|
-
"@parcel/watcher": "^2.
|
|
77
|
-
"@solid-primitives/event-bus": "^1.1.
|
|
78
|
-
"@standard-community/standard-json": "^0.3.
|
|
79
|
-
"@standard-community/standard-openapi": "^0.2.
|
|
80
|
-
"@zip.js/zip.js": "^2.
|
|
81
|
-
"ai": "^
|
|
82
|
-
"chokidar": "^
|
|
83
|
-
"clipboardy": "^5.
|
|
84
|
-
"command-stream": "^
|
|
73
|
+
"@opencode-ai/sdk": "^1.18.33",
|
|
74
|
+
"@openrouter/ai-sdk-provider": "^3.1.0",
|
|
75
|
+
"@opentui/core": "0.5.12",
|
|
76
|
+
"@opentui/solid": "0.5.12",
|
|
77
|
+
"@parcel/watcher": "^2.6.0",
|
|
78
|
+
"@solid-primitives/event-bus": "^1.1.4",
|
|
79
|
+
"@standard-community/standard-json": "^0.3.6",
|
|
80
|
+
"@standard-community/standard-openapi": "^0.2.10",
|
|
81
|
+
"@zip.js/zip.js": "^2.18.2",
|
|
82
|
+
"ai": "^7.0.122",
|
|
83
|
+
"chokidar": "^5.0.0",
|
|
84
|
+
"clipboardy": "^5.3.2",
|
|
85
|
+
"command-stream": "^1.3.0",
|
|
85
86
|
"decimal.js": "^10.6.0",
|
|
86
|
-
"diff": "^
|
|
87
|
-
"fuzzysort": "^
|
|
88
|
-
"glob": "^
|
|
89
|
-
"gpt-tokenizer": "^
|
|
87
|
+
"diff": "^9.0.0",
|
|
88
|
+
"fuzzysort": "^4.0.2",
|
|
89
|
+
"glob": "^13.0.6",
|
|
90
|
+
"gpt-tokenizer": "^4.0.0",
|
|
90
91
|
"gray-matter": "^4.0.3",
|
|
91
|
-
"hono": "^4.
|
|
92
|
-
"hono-openapi": "^1.
|
|
93
|
-
"ignore": "^7.0.
|
|
92
|
+
"hono": "^4.13.11",
|
|
93
|
+
"hono-openapi": "^1.3.3",
|
|
94
|
+
"ignore": "^7.0.10",
|
|
94
95
|
"jsonc-parser": "^3.3.1",
|
|
95
|
-
"lino-arguments": "^0.
|
|
96
|
-
"lino-objects-codec": "^0.
|
|
96
|
+
"lino-arguments": "^0.3.0",
|
|
97
|
+
"lino-objects-codec": "^0.8.0",
|
|
97
98
|
"log-lazy": "^1.0.4",
|
|
98
|
-
"minimatch": "^10.
|
|
99
|
-
"remeda": "^2.
|
|
99
|
+
"minimatch": "^10.2.6",
|
|
100
|
+
"remeda": "^2.50.0",
|
|
100
101
|
"solid-js": "1.9.12",
|
|
101
|
-
"strip-ansi": "^7.
|
|
102
|
-
"test-anywhere": "^0.
|
|
103
|
-
"tree-sitter-bash": "^0.25.
|
|
104
|
-
"turndown": "^7.2.
|
|
105
|
-
"ulid": "^3.0.
|
|
106
|
-
"vscode-jsonrpc": "^
|
|
107
|
-
"vscode-languageserver-types": "^3.
|
|
108
|
-
"web-tree-sitter": "
|
|
102
|
+
"strip-ansi": "^7.2.0",
|
|
103
|
+
"test-anywhere": "^0.9.1",
|
|
104
|
+
"tree-sitter-bash": "^0.25.1",
|
|
105
|
+
"turndown": "^7.2.4",
|
|
106
|
+
"ulid": "^3.0.2",
|
|
107
|
+
"vscode-jsonrpc": "^9.0.3",
|
|
108
|
+
"vscode-languageserver-types": "^3.18.4",
|
|
109
|
+
"web-tree-sitter": "0.25.10",
|
|
109
110
|
"xdg-basedir": "^5.1.0",
|
|
110
|
-
"yargs": "^18.
|
|
111
|
-
"zod": "^4.
|
|
111
|
+
"yargs": "^18.2.0",
|
|
112
|
+
"zod": "^4.6.5"
|
|
112
113
|
},
|
|
113
114
|
"devDependencies": {
|
|
114
|
-
"@changesets/cli": "^
|
|
115
|
-
"@eslint/js": "^
|
|
116
|
-
"eslint": "^
|
|
115
|
+
"@changesets/cli": "^3.0.3",
|
|
116
|
+
"@eslint/js": "^10.0.1",
|
|
117
|
+
"eslint": "^10.11.0",
|
|
117
118
|
"eslint-config-prettier": "^10.1.8",
|
|
118
|
-
"eslint-plugin-prettier": "^5.5.
|
|
119
|
-
"eslint-plugin-promise": "^7.
|
|
119
|
+
"eslint-plugin-prettier": "^5.5.6",
|
|
120
|
+
"eslint-plugin-promise": "^7.3.0",
|
|
120
121
|
"husky": "^9.1.7",
|
|
121
|
-
"lint-staged": "^
|
|
122
|
-
"prettier": "^3.
|
|
122
|
+
"lint-staged": "^17.6.0",
|
|
123
|
+
"prettier": "^3.9.9"
|
|
123
124
|
},
|
|
124
125
|
"lint-staged": {
|
|
125
126
|
"*.{js,mjs,cjs}": [
|
|
@@ -134,10 +135,15 @@
|
|
|
134
135
|
},
|
|
135
136
|
"license": "Unlicense",
|
|
136
137
|
"allowScripts": {
|
|
137
|
-
"@parcel/watcher@2.
|
|
138
|
-
"tree-sitter-bash@0.25.1": true
|
|
138
|
+
"@parcel/watcher@2.6.0": true,
|
|
139
|
+
"tree-sitter-bash@0.25.1": true,
|
|
140
|
+
"node-pty@1.2.0-beta.15": true
|
|
139
141
|
},
|
|
140
142
|
"overrides": {
|
|
141
143
|
"undici": "^7.16.0"
|
|
144
|
+
},
|
|
145
|
+
"dependencyPins": {
|
|
146
|
+
"solid-js": "https://github.com/anomalyco/opentui/issues/689",
|
|
147
|
+
"web-tree-sitter": "https://github.com/link-assistant/agent/issues/322"
|
|
142
148
|
}
|
|
143
149
|
}
|
package/src/agent/agent.ts
CHANGED
|
@@ -138,10 +138,10 @@ export namespace Agent {
|
|
|
138
138
|
const existing = await list();
|
|
139
139
|
const result = await generateObject({
|
|
140
140
|
temperature: 0.3,
|
|
141
|
-
// The system prompt belongs in `
|
|
141
|
+
// The system prompt belongs in `instructions`, not in `prompt`: the AI SDK
|
|
142
142
|
// warns that a system message inside the prompt enables prompt
|
|
143
143
|
// injection, and providers are free to treat it as ordinary content.
|
|
144
|
-
|
|
144
|
+
instructions: system.map((content) => ({
|
|
145
145
|
role: 'system' as const,
|
|
146
146
|
content,
|
|
147
147
|
})),
|
package/src/cli/input-queue.js
CHANGED
package/src/cli/model-config.js
CHANGED
|
@@ -100,6 +100,10 @@ export async function parseModelConfig(
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
if (argv['dry-run'] && !cliModelArg && modelArg === defaultModel) {
|
|
104
|
+
modelArg = 'link-assistant/echo';
|
|
105
|
+
}
|
|
106
|
+
|
|
103
107
|
// Attestation inputs (#295): record what was requested, and where it came
|
|
104
108
|
// from, before resolution can rewrite the effective model. `argv.model` is
|
|
105
109
|
// only treated as a request when it differs from the default, because yargs
|
|
@@ -358,6 +362,52 @@ async function resolveCompactionModelEntry(
|
|
|
358
362
|
};
|
|
359
363
|
}
|
|
360
364
|
|
|
365
|
+
/**
|
|
366
|
+
* Resolve a built-in cascade entry against the run's own provider only.
|
|
367
|
+
*
|
|
368
|
+
* The built-in cascade is narrowed to the configured provider anyway (#307),
|
|
369
|
+
* so resolving its short names across the whole provider registry only to
|
|
370
|
+
* drop the result afterwards was wasted work — and it logged records such as
|
|
371
|
+
* `{"providerID":"opencode","message":"resolved short model name"}` that a
|
|
372
|
+
* host reads as the run using OpenCode (#313). Only the base provider's own
|
|
373
|
+
* catalog is consulted here; any other provider is never looked up.
|
|
374
|
+
*
|
|
375
|
+
* @returns {Promise<{providerID: string, modelID: string, useSameModel: boolean} | null>}
|
|
376
|
+
* the entry, or `null` when the base provider does not serve it
|
|
377
|
+
* @see https://github.com/link-assistant/agent/issues/313
|
|
378
|
+
*/
|
|
379
|
+
async function resolveEntryWithinProvider(
|
|
380
|
+
modelArg,
|
|
381
|
+
baseProviderID,
|
|
382
|
+
baseModelID
|
|
383
|
+
) {
|
|
384
|
+
if (modelArg.toLowerCase() === 'same') {
|
|
385
|
+
return {
|
|
386
|
+
providerID: baseProviderID,
|
|
387
|
+
modelID: baseModelID,
|
|
388
|
+
useSameModel: true,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (modelArg.includes('/')) {
|
|
393
|
+
const parts = modelArg.split('/');
|
|
394
|
+
if (parts[0] !== baseProviderID) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
providerID: parts[0],
|
|
399
|
+
modelID: parts.slice(1).join('/'),
|
|
400
|
+
useSameModel: false,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const { Provider } = await import('../provider/provider.ts');
|
|
405
|
+
if (!(await Provider.hasModel(baseProviderID, modelArg))) {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
return { providerID: baseProviderID, modelID: modelArg, useSameModel: false };
|
|
409
|
+
}
|
|
410
|
+
|
|
361
411
|
/**
|
|
362
412
|
* Narrow a built-in compaction default to the provider the run was pointed at.
|
|
363
413
|
*
|
|
@@ -376,19 +426,17 @@ async function resolveCompactionModelEntry(
|
|
|
376
426
|
* provider keeps the shipped cascade, whose extra entries are the rate-limit
|
|
377
427
|
* fallbacks it was written for.
|
|
378
428
|
*
|
|
379
|
-
* @param {{providerID: string, modelID: string, useSameModel: boolean}[]}
|
|
429
|
+
* @param {{providerID: string, modelID: string, useSameModel: boolean}[]} kept
|
|
430
|
+
* Entries served by the base provider
|
|
431
|
+
* @param {string[]} dropped Default entries, as written, that were left out
|
|
380
432
|
* @param {string} baseProviderID Provider of the model that runs the turns
|
|
381
433
|
* @param {string} baseModelID Model that runs the turns
|
|
382
434
|
* @returns {{providerID: string, modelID: string, useSameModel: boolean}[]}
|
|
383
435
|
* @see https://github.com/link-assistant/agent/issues/307
|
|
384
436
|
*/
|
|
385
|
-
function inheritBaseProvider(
|
|
386
|
-
const kept = entries.filter(
|
|
387
|
-
(entry) => entry.useSameModel || entry.providerID === baseProviderID
|
|
388
|
-
);
|
|
389
|
-
const dropped = entries.filter((entry) => !kept.includes(entry));
|
|
437
|
+
function inheritBaseProvider(kept, dropped, baseProviderID, baseModelID) {
|
|
390
438
|
if (dropped.length === 0) {
|
|
391
|
-
return
|
|
439
|
+
return kept;
|
|
392
440
|
}
|
|
393
441
|
|
|
394
442
|
const inherited =
|
|
@@ -407,7 +455,9 @@ function inheritBaseProvider(entries, baseProviderID, baseModelID) {
|
|
|
407
455
|
hint: 'Secondary calls (compaction, session summary) inherit --model unless a compaction model is named explicitly',
|
|
408
456
|
baseProviderID,
|
|
409
457
|
baseModelID,
|
|
410
|
-
|
|
458
|
+
// The names as written in the default: they were never resolved, so they
|
|
459
|
+
// say nothing about which provider would have served them (#313).
|
|
460
|
+
dropped,
|
|
411
461
|
kept: inherited.map((entry) =>
|
|
412
462
|
entry.useSameModel ? 'same' : `${entry.providerID}/${entry.modelID}`
|
|
413
463
|
),
|
|
@@ -459,9 +509,30 @@ async function parseCompactionModelConfig(
|
|
|
459
509
|
const modelNames = parseLinksNotationSequence(compactionModelsArg);
|
|
460
510
|
|
|
461
511
|
if (modelNames.length > 0) {
|
|
462
|
-
//
|
|
512
|
+
// A cascade nobody asked for must not reach across providers (#307), and
|
|
513
|
+
// is not even resolved against other providers (#313).
|
|
514
|
+
const builtInCascade =
|
|
515
|
+
compactionModelsSource === 'default' &&
|
|
516
|
+
getDefaultCompactionModelsSource(defaultOptions) === 'default';
|
|
517
|
+
const usingBuiltInCascade =
|
|
518
|
+
builtInCascade && baseProviderID !== DEFAULT_PROVIDER_ID;
|
|
519
|
+
|
|
463
520
|
const compactionModels = [];
|
|
521
|
+
const unavailable = [];
|
|
464
522
|
for (const name of modelNames) {
|
|
523
|
+
if (usingBuiltInCascade) {
|
|
524
|
+
const entry = await resolveEntryWithinProvider(
|
|
525
|
+
name,
|
|
526
|
+
baseProviderID,
|
|
527
|
+
baseModelID
|
|
528
|
+
);
|
|
529
|
+
if (entry) {
|
|
530
|
+
compactionModels.push(entry);
|
|
531
|
+
} else {
|
|
532
|
+
unavailable.push(name);
|
|
533
|
+
}
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
465
536
|
try {
|
|
466
537
|
const resolved = await resolveCompactionModelEntry(
|
|
467
538
|
name,
|
|
@@ -474,6 +545,12 @@ async function parseCompactionModelConfig(
|
|
|
474
545
|
useSameModel: resolved.useSameModel,
|
|
475
546
|
});
|
|
476
547
|
} catch (err) {
|
|
548
|
+
// A model nobody asked for is not an error worth a record of its
|
|
549
|
+
// own (#313); the summary below lists it.
|
|
550
|
+
if (builtInCascade) {
|
|
551
|
+
unavailable.push(name);
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
477
554
|
const logSkip =
|
|
478
555
|
compactionModelsSource === 'default'
|
|
479
556
|
? Log.Default.debug
|
|
@@ -486,13 +563,13 @@ async function parseCompactionModelConfig(
|
|
|
486
563
|
}
|
|
487
564
|
}
|
|
488
565
|
|
|
489
|
-
// A cascade nobody asked for must not reach across providers (#307).
|
|
490
|
-
const usingBuiltInCascade =
|
|
491
|
-
compactionModelsSource === 'default' &&
|
|
492
|
-
getDefaultCompactionModelsSource(defaultOptions) === 'default' &&
|
|
493
|
-
baseProviderID !== DEFAULT_PROVIDER_ID;
|
|
494
566
|
const cascade = usingBuiltInCascade
|
|
495
|
-
? inheritBaseProvider(
|
|
567
|
+
? inheritBaseProvider(
|
|
568
|
+
compactionModels,
|
|
569
|
+
unavailable,
|
|
570
|
+
baseProviderID,
|
|
571
|
+
baseModelID
|
|
572
|
+
)
|
|
496
573
|
: compactionModels;
|
|
497
574
|
|
|
498
575
|
Log.Default.info(() => ({
|
|
@@ -501,6 +578,9 @@ async function parseCompactionModelConfig(
|
|
|
501
578
|
m.useSameModel ? 'same' : `${m.providerID}/${m.modelID}`
|
|
502
579
|
),
|
|
503
580
|
source: compactionModelsSource,
|
|
581
|
+
...(unavailable.length > 0 && !usingBuiltInCascade
|
|
582
|
+
? { unavailable }
|
|
583
|
+
: {}),
|
|
504
584
|
}));
|
|
505
585
|
|
|
506
586
|
// Use the first model as the primary compaction model (for backward compatibility)
|
|
@@ -543,9 +623,17 @@ async function parseCompactionModelConfig(
|
|
|
543
623
|
compactionModelSource === 'default' &&
|
|
544
624
|
getDefaultCompactionModelSource(defaultOptions) === 'default' &&
|
|
545
625
|
baseProviderID !== DEFAULT_PROVIDER_ID;
|
|
546
|
-
const [entry] =
|
|
547
|
-
|
|
548
|
-
|
|
626
|
+
const [entry] =
|
|
627
|
+
usingBuiltInCompactionModel &&
|
|
628
|
+
!resolved.useSameModel &&
|
|
629
|
+
resolved.providerID !== baseProviderID
|
|
630
|
+
? inheritBaseProvider(
|
|
631
|
+
[],
|
|
632
|
+
[compactionModelArg],
|
|
633
|
+
baseProviderID,
|
|
634
|
+
baseModelID
|
|
635
|
+
)
|
|
636
|
+
: [resolved];
|
|
549
637
|
|
|
550
638
|
Log.Default.info(() => ({
|
|
551
639
|
message: 'using single compaction model',
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Host integration contract: how an embedding host hands the CLI its config.
|
|
6
|
+
*
|
|
7
|
+
* A host supplies configuration through the CLI's own variables:
|
|
8
|
+
*
|
|
9
|
+
* LINK_ASSISTANT_AGENT_CONFIG_DIR a directory holding opencode.json(c)
|
|
10
|
+
* LINK_ASSISTANT_AGENT_CONFIG a single config file
|
|
11
|
+
* LINK_ASSISTANT_AGENT_CONFIG_CONTENT the config itself, as JSON
|
|
12
|
+
*
|
|
13
|
+
* Relocating `XDG_CONFIG_HOME` to feed the CLI a generated config also moves
|
|
14
|
+
* the config home of every tool the agent runs — `gh` then finds no
|
|
15
|
+
* `hosts.yml` and the session loses GitHub authentication (#314). This module
|
|
16
|
+
* spots that setup so the CLI can point the host at the variables above.
|
|
17
|
+
*
|
|
18
|
+
* @see https://github.com/link-assistant/agent/issues/314
|
|
19
|
+
* @see ../../../docs/host-integration.md
|
|
20
|
+
*/
|
|
21
|
+
export namespace HostConfigHint {
|
|
22
|
+
export const CONFIG_ENV_VARS = [
|
|
23
|
+
'LINK_ASSISTANT_AGENT_CONFIG_DIR',
|
|
24
|
+
'LINK_ASSISTANT_AGENT_CONFIG',
|
|
25
|
+
'LINK_ASSISTANT_AGENT_CONFIG_CONTENT',
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
export interface Hint {
|
|
29
|
+
message: string;
|
|
30
|
+
xdgConfigHome: string;
|
|
31
|
+
realConfigHome: string;
|
|
32
|
+
hint: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Return a hint when `XDG_CONFIG_HOME` points away from the user's real
|
|
37
|
+
* config home and none of the CLI's own config variables is set — the
|
|
38
|
+
* signature of a host relocating XDG to deliver config. Pure, so it can be
|
|
39
|
+
* tested with any environment.
|
|
40
|
+
*/
|
|
41
|
+
export function detect(
|
|
42
|
+
env: Record<string, string | undefined> = process.env,
|
|
43
|
+
homedir: string = os.homedir()
|
|
44
|
+
): Hint | undefined {
|
|
45
|
+
const xdgConfigHome = env.XDG_CONFIG_HOME?.trim();
|
|
46
|
+
if (!xdgConfigHome) return undefined;
|
|
47
|
+
|
|
48
|
+
const realConfigHome = path.join(homedir, '.config');
|
|
49
|
+
if (path.resolve(xdgConfigHome) === path.resolve(realConfigHome)) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (CONFIG_ENV_VARS.some((name) => env[name]?.trim())) return undefined;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
message: 'XDG_CONFIG_HOME is relocated',
|
|
57
|
+
xdgConfigHome,
|
|
58
|
+
realConfigHome,
|
|
59
|
+
hint: `To supply agent config, set ${CONFIG_ENV_VARS.join(', ')} instead of XDG_CONFIG_HOME: tools the agent runs (gh, git) read their config and credentials from XDG_CONFIG_HOME too`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
getConfigSnapshot,
|
|
8
8
|
} from './config/config.ts';
|
|
9
9
|
import { setProcessName } from './cli/process-name.ts';
|
|
10
|
+
import { HostConfigHint } from './config/host-config-hint.ts';
|
|
10
11
|
setProcessName('agent');
|
|
11
12
|
import { Server } from './server/server.ts';
|
|
12
13
|
import { Instance } from './project/instance.ts';
|
|
@@ -909,6 +910,13 @@ async function main() {
|
|
|
909
910
|
config: getConfigSnapshot(),
|
|
910
911
|
}));
|
|
911
912
|
|
|
913
|
+
// A host that relocates XDG_CONFIG_HOME to deliver agent config also
|
|
914
|
+
// hides gh/git credentials from the agent's tool calls (#314).
|
|
915
|
+
const hostConfigHint = HostConfigHint.detect();
|
|
916
|
+
if (hostConfigHint) {
|
|
917
|
+
Log.Default.warn(() => hostConfigHint);
|
|
918
|
+
}
|
|
919
|
+
|
|
912
920
|
// Global fetch monkey-patch for verbose HTTP logging (#221).
|
|
913
921
|
if (!globalThis.__agentVerboseFetchInstalled) {
|
|
914
922
|
globalThis.fetch = createVerboseFetch(globalThis.fetch, {
|
package/src/provider/cache.ts
CHANGED
|
@@ -15,7 +15,11 @@
|
|
|
15
15
|
* @see https://github.com/link-foundation/lino-objects-codec
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import type {
|
|
18
|
+
import type {
|
|
19
|
+
LanguageModelV4,
|
|
20
|
+
LanguageModelV4CallOptions,
|
|
21
|
+
LanguageModelV4GenerateResult,
|
|
22
|
+
} from '@ai-sdk/provider';
|
|
19
23
|
import { Log } from '../util/log';
|
|
20
24
|
import { createEchoModel } from './echo';
|
|
21
25
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
@@ -33,7 +37,7 @@ const CACHE_ROOT = join(__dirname, '../../data/api-cache');
|
|
|
33
37
|
* Generate a cache key from the prompt
|
|
34
38
|
*/
|
|
35
39
|
function generateCacheKey(
|
|
36
|
-
prompt:
|
|
40
|
+
prompt: LanguageModelV4CallOptions['prompt']
|
|
37
41
|
): string {
|
|
38
42
|
// Simple hash of the prompt content
|
|
39
43
|
const content = JSON.stringify(prompt);
|
|
@@ -64,14 +68,43 @@ function generatePartId(): string {
|
|
|
64
68
|
/**
|
|
65
69
|
* Load cached response from file using Links Notation format
|
|
66
70
|
*/
|
|
67
|
-
function loadCachedResponse(
|
|
71
|
+
function loadCachedResponse(
|
|
72
|
+
filePath: string
|
|
73
|
+
): LanguageModelV4GenerateResult | null {
|
|
68
74
|
try {
|
|
69
75
|
if (!existsSync(filePath)) {
|
|
70
76
|
return null;
|
|
71
77
|
}
|
|
72
78
|
const content = readFileSync(filePath, 'utf8');
|
|
73
79
|
// Decode from Links Notation format
|
|
74
|
-
|
|
80
|
+
const cached = decode({ notation: content });
|
|
81
|
+
// Existing .lino files may contain AI SDK 6 responses. Normalize their
|
|
82
|
+
// finish reason and usage so they remain usable by AI SDK 7.
|
|
83
|
+
if (typeof cached?.finishReason === 'string') {
|
|
84
|
+
const promptTokens = cached.usage?.promptTokens ?? 0;
|
|
85
|
+
const completionTokens = cached.usage?.completionTokens ?? 0;
|
|
86
|
+
return {
|
|
87
|
+
...cached,
|
|
88
|
+
finishReason: {
|
|
89
|
+
unified: cached.finishReason,
|
|
90
|
+
raw: cached.finishReason,
|
|
91
|
+
},
|
|
92
|
+
usage: {
|
|
93
|
+
inputTokens: {
|
|
94
|
+
total: promptTokens,
|
|
95
|
+
noCache: promptTokens,
|
|
96
|
+
cacheRead: 0,
|
|
97
|
+
cacheWrite: 0,
|
|
98
|
+
},
|
|
99
|
+
outputTokens: {
|
|
100
|
+
total: completionTokens,
|
|
101
|
+
text: completionTokens,
|
|
102
|
+
reasoning: 0,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return cached;
|
|
75
108
|
} catch (error: any) {
|
|
76
109
|
log.warn('Failed to load cached response', {
|
|
77
110
|
filePath,
|
|
@@ -104,21 +137,21 @@ function saveCachedResponse(filePath: string, response: any): void {
|
|
|
104
137
|
|
|
105
138
|
/**
|
|
106
139
|
* Creates a cache language model that stores/retrieves responses
|
|
107
|
-
* Implements
|
|
140
|
+
* Implements the AI SDK 7 language model interface.
|
|
108
141
|
*/
|
|
109
142
|
export function createCacheModel(
|
|
110
143
|
providerId: string,
|
|
111
144
|
modelId: string
|
|
112
|
-
):
|
|
113
|
-
const model:
|
|
114
|
-
specificationVersion: '
|
|
145
|
+
): LanguageModelV4 {
|
|
146
|
+
const model: LanguageModelV4 = {
|
|
147
|
+
specificationVersion: 'v4',
|
|
115
148
|
provider: 'link-assistant',
|
|
116
149
|
modelId: `${providerId}/${modelId}`,
|
|
117
150
|
|
|
118
151
|
// No external URLs are supported by this synthetic provider
|
|
119
152
|
supportedUrls: {},
|
|
120
153
|
|
|
121
|
-
async doGenerate(options:
|
|
154
|
+
async doGenerate(options: LanguageModelV4CallOptions) {
|
|
122
155
|
const cacheKey = generateCacheKey(options.prompt);
|
|
123
156
|
const cachePath = getCachePath(providerId, modelId, cacheKey);
|
|
124
157
|
|
|
@@ -144,7 +177,7 @@ export function createCacheModel(
|
|
|
144
177
|
return response;
|
|
145
178
|
},
|
|
146
179
|
|
|
147
|
-
async doStream(options:
|
|
180
|
+
async doStream(options: LanguageModelV4CallOptions) {
|
|
148
181
|
const cacheKey = generateCacheKey(options.prompt);
|
|
149
182
|
const cachePath = getCachePath(providerId, modelId, cacheKey);
|
|
150
183
|
|
|
@@ -163,7 +196,7 @@ export function createCacheModel(
|
|
|
163
196
|
cached.content?.[0]?.text || cached.text || 'Cached response';
|
|
164
197
|
const textPartId = generatePartId();
|
|
165
198
|
|
|
166
|
-
// Create a ReadableStream with
|
|
199
|
+
// Create a ReadableStream with LanguageModelV4StreamPart format.
|
|
167
200
|
const stream = new ReadableStream({
|
|
168
201
|
async start(controller) {
|
|
169
202
|
// Emit text-start
|
|
@@ -195,10 +228,19 @@ export function createCacheModel(
|
|
|
195
228
|
// Emit finish event
|
|
196
229
|
controller.enqueue({
|
|
197
230
|
type: 'finish',
|
|
198
|
-
finishReason: 'stop',
|
|
231
|
+
finishReason: { unified: 'stop', raw: 'stop' },
|
|
199
232
|
usage: cached.usage || {
|
|
200
|
-
|
|
201
|
-
|
|
233
|
+
inputTokens: {
|
|
234
|
+
total: Math.ceil(echoText.length / 4),
|
|
235
|
+
noCache: Math.ceil(echoText.length / 4),
|
|
236
|
+
cacheRead: 0,
|
|
237
|
+
cacheWrite: 0,
|
|
238
|
+
},
|
|
239
|
+
outputTokens: {
|
|
240
|
+
total: Math.ceil(echoText.length / 4),
|
|
241
|
+
text: Math.ceil(echoText.length / 4),
|
|
242
|
+
reasoning: 0,
|
|
243
|
+
},
|
|
202
244
|
},
|
|
203
245
|
providerMetadata: undefined,
|
|
204
246
|
});
|
|
@@ -211,7 +253,6 @@ export function createCacheModel(
|
|
|
211
253
|
stream,
|
|
212
254
|
request: undefined,
|
|
213
255
|
response: undefined,
|
|
214
|
-
warnings: [],
|
|
215
256
|
};
|
|
216
257
|
}
|
|
217
258
|
|
|
@@ -237,7 +278,7 @@ export function createCacheModel(
|
|
|
237
278
|
*/
|
|
238
279
|
export function createCacheProvider(options?: { name?: string }) {
|
|
239
280
|
return {
|
|
240
|
-
languageModel(modelId: string):
|
|
281
|
+
languageModel(modelId: string): LanguageModelV4 {
|
|
241
282
|
// Parse provider/model from modelId like "opencode/grok-code"
|
|
242
283
|
const parts = modelId.split('/');
|
|
243
284
|
if (parts.length < 2) {
|
package/src/provider/echo.ts
CHANGED
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
* Input: "How are you?" -> Output: "How are you?"
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import type {
|
|
17
|
+
import type {
|
|
18
|
+
LanguageModelV4,
|
|
19
|
+
LanguageModelV4CallOptions,
|
|
20
|
+
} from '@ai-sdk/provider';
|
|
18
21
|
import { Log } from '../util/log';
|
|
19
22
|
|
|
20
23
|
const log = Log.create({ service: 'provider.echo' });
|
|
@@ -23,7 +26,7 @@ const log = Log.create({ service: 'provider.echo' });
|
|
|
23
26
|
* Extract text content from the prompt messages
|
|
24
27
|
*/
|
|
25
28
|
function extractTextFromPrompt(
|
|
26
|
-
prompt:
|
|
29
|
+
prompt: LanguageModelV4CallOptions['prompt']
|
|
27
30
|
): string {
|
|
28
31
|
const textParts: string[] = [];
|
|
29
32
|
|
|
@@ -52,18 +55,18 @@ function generatePartId(): string {
|
|
|
52
55
|
|
|
53
56
|
/**
|
|
54
57
|
* Creates an echo language model that echoes back the user's input
|
|
55
|
-
* Implements
|
|
58
|
+
* Implements the AI SDK 7 language model interface.
|
|
56
59
|
*/
|
|
57
|
-
export function createEchoModel(modelId: string = 'echo'):
|
|
58
|
-
const model:
|
|
59
|
-
specificationVersion: '
|
|
60
|
+
export function createEchoModel(modelId: string = 'echo'): LanguageModelV4 {
|
|
61
|
+
const model: LanguageModelV4 = {
|
|
62
|
+
specificationVersion: 'v4',
|
|
60
63
|
provider: 'link-assistant',
|
|
61
64
|
modelId,
|
|
62
65
|
|
|
63
66
|
// No external URLs are supported by this synthetic provider
|
|
64
67
|
supportedUrls: {},
|
|
65
68
|
|
|
66
|
-
async doGenerate(options:
|
|
69
|
+
async doGenerate(options: LanguageModelV4CallOptions) {
|
|
67
70
|
const echoText = extractTextFromPrompt(options.prompt);
|
|
68
71
|
log.info('echo generate', { modelId, echoText });
|
|
69
72
|
|
|
@@ -78,10 +81,19 @@ export function createEchoModel(modelId: string = 'echo'): LanguageModelV2 {
|
|
|
78
81
|
text: echoText,
|
|
79
82
|
},
|
|
80
83
|
],
|
|
81
|
-
finishReason: 'stop' as const,
|
|
84
|
+
finishReason: { unified: 'stop' as const, raw: 'stop' },
|
|
82
85
|
usage: {
|
|
83
|
-
|
|
84
|
-
|
|
86
|
+
inputTokens: {
|
|
87
|
+
total: promptTokens,
|
|
88
|
+
noCache: promptTokens,
|
|
89
|
+
cacheRead: 0,
|
|
90
|
+
cacheWrite: 0,
|
|
91
|
+
},
|
|
92
|
+
outputTokens: {
|
|
93
|
+
total: completionTokens,
|
|
94
|
+
text: completionTokens,
|
|
95
|
+
reasoning: 0,
|
|
96
|
+
},
|
|
85
97
|
},
|
|
86
98
|
warnings: [],
|
|
87
99
|
providerMetadata: undefined,
|
|
@@ -90,7 +102,7 @@ export function createEchoModel(modelId: string = 'echo'): LanguageModelV2 {
|
|
|
90
102
|
};
|
|
91
103
|
},
|
|
92
104
|
|
|
93
|
-
async doStream(options:
|
|
105
|
+
async doStream(options: LanguageModelV4CallOptions) {
|
|
94
106
|
const echoText = extractTextFromPrompt(options.prompt);
|
|
95
107
|
log.info('echo stream', { modelId, echoText });
|
|
96
108
|
|
|
@@ -100,8 +112,7 @@ export function createEchoModel(modelId: string = 'echo'): LanguageModelV2 {
|
|
|
100
112
|
|
|
101
113
|
const textPartId = generatePartId();
|
|
102
114
|
|
|
103
|
-
//
|
|
104
|
-
// V2 format uses: text-start -> text-delta (with delta) -> text-end -> finish
|
|
115
|
+
// Stream text-start -> text-delta -> text-end -> finish.
|
|
105
116
|
const stream = new ReadableStream({
|
|
106
117
|
async start(controller) {
|
|
107
118
|
// Emit text-start
|
|
@@ -133,10 +144,19 @@ export function createEchoModel(modelId: string = 'echo'): LanguageModelV2 {
|
|
|
133
144
|
// Emit finish event with usage information
|
|
134
145
|
controller.enqueue({
|
|
135
146
|
type: 'finish',
|
|
136
|
-
finishReason: 'stop',
|
|
147
|
+
finishReason: { unified: 'stop', raw: 'stop' },
|
|
137
148
|
usage: {
|
|
138
|
-
|
|
139
|
-
|
|
149
|
+
inputTokens: {
|
|
150
|
+
total: promptTokens,
|
|
151
|
+
noCache: promptTokens,
|
|
152
|
+
cacheRead: 0,
|
|
153
|
+
cacheWrite: 0,
|
|
154
|
+
},
|
|
155
|
+
outputTokens: {
|
|
156
|
+
total: completionTokens,
|
|
157
|
+
text: completionTokens,
|
|
158
|
+
reasoning: 0,
|
|
159
|
+
},
|
|
140
160
|
},
|
|
141
161
|
providerMetadata: undefined,
|
|
142
162
|
});
|
|
@@ -149,7 +169,6 @@ export function createEchoModel(modelId: string = 'echo'): LanguageModelV2 {
|
|
|
149
169
|
stream,
|
|
150
170
|
request: undefined,
|
|
151
171
|
response: undefined,
|
|
152
|
-
warnings: [],
|
|
153
172
|
};
|
|
154
173
|
},
|
|
155
174
|
};
|
|
@@ -162,7 +181,7 @@ export function createEchoModel(modelId: string = 'echo'): LanguageModelV2 {
|
|
|
162
181
|
*/
|
|
163
182
|
export function createEchoProvider(options?: { name?: string }) {
|
|
164
183
|
return {
|
|
165
|
-
languageModel(modelId: string):
|
|
184
|
+
languageModel(modelId: string): LanguageModelV4 {
|
|
166
185
|
return createEchoModel(modelId);
|
|
167
186
|
},
|
|
168
187
|
textEmbeddingModel() {
|
|
@@ -90,8 +90,7 @@ export namespace OpenCodeZen {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
const body = (await response.json().catch(() => undefined)) as
|
|
93
|
-
|
|
94
|
-
| undefined;
|
|
93
|
+
{ data?: Array<{ id?: unknown }> } | undefined;
|
|
95
94
|
const rows = Array.isArray(body?.data) ? body.data : [];
|
|
96
95
|
return new Set(
|
|
97
96
|
rows
|
package/src/provider/provider.ts
CHANGED
|
@@ -38,6 +38,14 @@ import { createGroq } from '@ai-sdk/groq';
|
|
|
38
38
|
|
|
39
39
|
export namespace Provider {
|
|
40
40
|
const log = Log.create({ service: 'provider' });
|
|
41
|
+
/**
|
|
42
|
+
* Catalog lookups — which providers are installed, which provider a short
|
|
43
|
+
* model name would map to — describe candidates, not the model a run uses.
|
|
44
|
+
* They are logged under their own service and never carry `providerID` /
|
|
45
|
+
* `modelID` fields, which stream consumers read as the run's model identity
|
|
46
|
+
* (#313). The active model is attested by the `model_resolved` event.
|
|
47
|
+
*/
|
|
48
|
+
const registryLog = Log.create({ service: 'provider-registry' });
|
|
41
49
|
|
|
42
50
|
/**
|
|
43
51
|
* Bundled providers - these are pre-installed and imported statically
|
|
@@ -722,9 +730,9 @@ export namespace Provider {
|
|
|
722
730
|
'link-assistant': async () => {
|
|
723
731
|
// Echo provider is always available - no external dependencies needed
|
|
724
732
|
return {
|
|
725
|
-
autoload:
|
|
733
|
+
autoload: true, // Also available through --model link-assistant/echo
|
|
726
734
|
async getModel(_sdk: any, modelID: string) {
|
|
727
|
-
// Return our custom
|
|
735
|
+
// Return our custom AI SDK language model.
|
|
728
736
|
return createEchoModel(modelID);
|
|
729
737
|
},
|
|
730
738
|
options: {},
|
|
@@ -754,7 +762,7 @@ export namespace Provider {
|
|
|
754
762
|
const [providerId, ...modelParts] = parts;
|
|
755
763
|
const actualModelId = modelParts.join('/');
|
|
756
764
|
|
|
757
|
-
// Return our custom
|
|
765
|
+
// Return our custom AI SDK language model.
|
|
758
766
|
return createCacheModel(providerId, actualModelId);
|
|
759
767
|
},
|
|
760
768
|
options: {},
|
|
@@ -1235,8 +1243,11 @@ export namespace Provider {
|
|
|
1235
1243
|
delete providers[providerID];
|
|
1236
1244
|
continue;
|
|
1237
1245
|
}
|
|
1238
|
-
log.info(() => ({ message: 'found', providerID }));
|
|
1239
1246
|
}
|
|
1247
|
+
registryLog.info(() => ({
|
|
1248
|
+
message: 'providers available',
|
|
1249
|
+
available: Object.keys(providers),
|
|
1250
|
+
}));
|
|
1240
1251
|
|
|
1241
1252
|
return {
|
|
1242
1253
|
models,
|
|
@@ -1696,6 +1707,15 @@ export namespace Provider {
|
|
|
1696
1707
|
return state().then((s) => s.providers[providerID]);
|
|
1697
1708
|
}
|
|
1698
1709
|
|
|
1710
|
+
/**
|
|
1711
|
+
* Whether a provider's catalog lists a model. Looks at that one provider
|
|
1712
|
+
* only and logs nothing (#313).
|
|
1713
|
+
*/
|
|
1714
|
+
export async function hasModel(providerID: string, modelID: string) {
|
|
1715
|
+
const provider = await getProvider(providerID);
|
|
1716
|
+
return Boolean(provider?.info.models[modelID]);
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1699
1719
|
async function getLiveModelInfo(providerID: string, modelID: string) {
|
|
1700
1720
|
if (providerID !== 'opencode') return undefined;
|
|
1701
1721
|
return OpenCodeZen.getLiveFreeModelInfo(modelID);
|
|
@@ -1752,8 +1772,16 @@ export namespace Provider {
|
|
|
1752
1772
|
const isSyntheticProvider =
|
|
1753
1773
|
providerID === 'link-assistant' || providerID === 'link-assistant/cache';
|
|
1754
1774
|
|
|
1755
|
-
//
|
|
1756
|
-
let info =
|
|
1775
|
+
// Synthetic models still need metadata for context limits and accounting.
|
|
1776
|
+
let info =
|
|
1777
|
+
provider.info.models[modelID] ??
|
|
1778
|
+
(isSyntheticProvider
|
|
1779
|
+
? {
|
|
1780
|
+
...s.providers['link-assistant'].info.models.echo,
|
|
1781
|
+
id: modelID,
|
|
1782
|
+
name: modelID,
|
|
1783
|
+
}
|
|
1784
|
+
: undefined);
|
|
1757
1785
|
if (!isSyntheticProvider && !info) {
|
|
1758
1786
|
// Model not in provider's known list - try refreshing the cache first (#200)
|
|
1759
1787
|
// This handles stale bundled data or expired cache (1-hour TTL)
|
|
@@ -2084,9 +2112,10 @@ export namespace Provider {
|
|
|
2084
2112
|
if (kiloUniqueModels.includes(modelID)) {
|
|
2085
2113
|
const kiloProvider = s.providers['kilo'];
|
|
2086
2114
|
if (kiloProvider && kiloProvider.info.models[modelID]) {
|
|
2087
|
-
|
|
2115
|
+
registryLog.info(() => ({
|
|
2088
2116
|
message: 'resolved short model name to kilo (unique)',
|
|
2089
|
-
modelID,
|
|
2117
|
+
name: modelID,
|
|
2118
|
+
candidate: `kilo/${modelID}`,
|
|
2090
2119
|
}));
|
|
2091
2120
|
return { providerID: 'kilo', modelID };
|
|
2092
2121
|
}
|
|
@@ -2095,9 +2124,10 @@ export namespace Provider {
|
|
|
2095
2124
|
if (modelID === 'formal-ai') {
|
|
2096
2125
|
const formalAiProvider = s.providers['formal-ai'];
|
|
2097
2126
|
if (formalAiProvider && formalAiProvider.info.models[modelID]) {
|
|
2098
|
-
|
|
2127
|
+
registryLog.info(() => ({
|
|
2099
2128
|
message: 'resolved short model name to formal-ai (canonical)',
|
|
2100
|
-
modelID,
|
|
2129
|
+
name: modelID,
|
|
2130
|
+
candidate: `formal-ai/${modelID}`,
|
|
2101
2131
|
}));
|
|
2102
2132
|
return { providerID: 'formal-ai', modelID };
|
|
2103
2133
|
}
|
|
@@ -2117,10 +2147,10 @@ export namespace Provider {
|
|
|
2117
2147
|
|
|
2118
2148
|
if (matchingProviders.length === 1) {
|
|
2119
2149
|
const providerID = matchingProviders[0];
|
|
2120
|
-
|
|
2150
|
+
registryLog.info(() => ({
|
|
2121
2151
|
message: 'resolved short model name (single match)',
|
|
2122
|
-
modelID,
|
|
2123
|
-
providerID
|
|
2152
|
+
name: modelID,
|
|
2153
|
+
candidate: `${providerID}/${modelID}`,
|
|
2124
2154
|
}));
|
|
2125
2155
|
return { providerID, modelID };
|
|
2126
2156
|
}
|
|
@@ -2128,9 +2158,10 @@ export namespace Provider {
|
|
|
2128
2158
|
// Multiple providers have this model - prefer OpenCode for shared free models
|
|
2129
2159
|
// This follows the convention that opencode is the primary free provider
|
|
2130
2160
|
if (matchingProviders.includes('opencode')) {
|
|
2131
|
-
|
|
2161
|
+
registryLog.info(() => ({
|
|
2132
2162
|
message: 'resolved short model name to opencode (multiple providers)',
|
|
2133
|
-
modelID,
|
|
2163
|
+
name: modelID,
|
|
2164
|
+
candidate: `opencode/${modelID}`,
|
|
2134
2165
|
availableProviders: matchingProviders,
|
|
2135
2166
|
}));
|
|
2136
2167
|
return { providerID: 'opencode', modelID };
|
|
@@ -2138,10 +2169,10 @@ export namespace Provider {
|
|
|
2138
2169
|
|
|
2139
2170
|
// Fallback to first matching provider
|
|
2140
2171
|
const providerID = matchingProviders[0];
|
|
2141
|
-
|
|
2172
|
+
registryLog.info(() => ({
|
|
2142
2173
|
message: 'resolved short model name (fallback)',
|
|
2143
|
-
modelID,
|
|
2144
|
-
providerID
|
|
2174
|
+
name: modelID,
|
|
2175
|
+
candidate: `${providerID}/${modelID}`,
|
|
2145
2176
|
availableProviders: matchingProviders,
|
|
2146
2177
|
}));
|
|
2147
2178
|
return { providerID, modelID };
|
|
@@ -387,7 +387,7 @@ export namespace SessionCompaction {
|
|
|
387
387
|
headers: model.info.headers,
|
|
388
388
|
abortSignal: input.abort,
|
|
389
389
|
tools: model.info.tool_call ? {} : undefined,
|
|
390
|
-
|
|
390
|
+
instructions: system.map((content) => ({ role: 'system', content })),
|
|
391
391
|
messages: [
|
|
392
392
|
...safeModelMessages,
|
|
393
393
|
{
|
package/src/session/processor.ts
CHANGED
|
@@ -116,7 +116,7 @@ export namespace SessionProcessor {
|
|
|
116
116
|
let reasoningMap: Record<string, MessageV2.ReasoningPart> = {};
|
|
117
117
|
const stream = fn();
|
|
118
118
|
|
|
119
|
-
for await (const value of stream.
|
|
119
|
+
for await (const value of stream.stream) {
|
|
120
120
|
input.abort.throwIfAborted();
|
|
121
121
|
switch (value.type) {
|
|
122
122
|
case 'start':
|
package/src/session/prompt.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
type Tool as AITool,
|
|
16
16
|
tool,
|
|
17
17
|
wrapLanguageModel,
|
|
18
|
-
|
|
18
|
+
isStepCount,
|
|
19
19
|
jsonSchema,
|
|
20
20
|
} from 'ai';
|
|
21
21
|
import { SessionCompaction } from './compaction';
|
|
@@ -1011,10 +1011,10 @@ export namespace SessionPrompt {
|
|
|
1011
1011
|
model.providerID,
|
|
1012
1012
|
params.options
|
|
1013
1013
|
),
|
|
1014
|
-
stopWhen:
|
|
1014
|
+
stopWhen: isStepCount(1),
|
|
1015
1015
|
temperature: params.temperature,
|
|
1016
1016
|
topP: params.topP,
|
|
1017
|
-
|
|
1017
|
+
instructions: system.map((content) => ({ role: 'system', content })),
|
|
1018
1018
|
messages: safeModelMessages,
|
|
1019
1019
|
tools: model.info?.tool_call === false ? undefined : tools,
|
|
1020
1020
|
model: wrapLanguageModel({
|
|
@@ -1059,6 +1059,9 @@ export namespace SessionPrompt {
|
|
|
1059
1059
|
if (input.model) {
|
|
1060
1060
|
return input.model;
|
|
1061
1061
|
}
|
|
1062
|
+
if (config.dryRun) {
|
|
1063
|
+
return Provider.defaultModel();
|
|
1064
|
+
}
|
|
1062
1065
|
if (input.agent.model) {
|
|
1063
1066
|
return input.agent.model;
|
|
1064
1067
|
}
|
|
@@ -1942,7 +1945,7 @@ export namespace SessionPrompt {
|
|
|
1942
1945
|
small.providerID,
|
|
1943
1946
|
options
|
|
1944
1947
|
),
|
|
1945
|
-
|
|
1948
|
+
instructions: safeTitleSystemMessages.map((content) => ({
|
|
1946
1949
|
role: 'system',
|
|
1947
1950
|
content,
|
|
1948
1951
|
})),
|
package/src/session/summary.ts
CHANGED
|
@@ -222,10 +222,10 @@ export namespace SessionSummary {
|
|
|
222
222
|
model.providerID,
|
|
223
223
|
{}
|
|
224
224
|
),
|
|
225
|
-
// The system prompt belongs in `
|
|
225
|
+
// The system prompt belongs in `instructions`, not in `messages`: the AI SDK
|
|
226
226
|
// warns that a system message inside `messages` enables prompt
|
|
227
227
|
// injection, and providers are free to treat it as ordinary content.
|
|
228
|
-
|
|
228
|
+
instructions: systemPrompts.map((content) => ({
|
|
229
229
|
role: 'system' as const,
|
|
230
230
|
content,
|
|
231
231
|
})),
|