@link-assistant/agent 0.25.8 → 0.26.1
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 +9 -4
- package/src/agent/agent.ts +8 -7
- 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/src/project/instance.ts +1 -0
- package/src/project/project.ts +66 -0
- package/src/session/summary.ts +8 -7
- package/src/snapshot/index.ts +19 -0
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.1",
|
|
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,8 +50,6 @@
|
|
|
50
50
|
"LICENSE"
|
|
51
51
|
],
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@actions/core": "^1.11.1",
|
|
54
|
-
"@actions/github": "^6.0.1",
|
|
55
53
|
"@agentclientprotocol/sdk": "^0.5.1",
|
|
56
54
|
"@ai-sdk/amazon-bedrock": "^3.0.74",
|
|
57
55
|
"@ai-sdk/anthropic": "^2.0.58",
|
|
@@ -134,5 +132,12 @@
|
|
|
134
132
|
"prettier --check"
|
|
135
133
|
]
|
|
136
134
|
},
|
|
137
|
-
"license": "Unlicense"
|
|
135
|
+
"license": "Unlicense",
|
|
136
|
+
"allowScripts": {
|
|
137
|
+
"@parcel/watcher@2.5.1": true,
|
|
138
|
+
"tree-sitter-bash@0.25.1": true
|
|
139
|
+
},
|
|
140
|
+
"overrides": {
|
|
141
|
+
"undici": "^7.16.0"
|
|
142
|
+
}
|
|
138
143
|
}
|
package/src/agent/agent.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Config } from '../config/file-config';
|
|
2
2
|
import z from 'zod';
|
|
3
3
|
import { Provider } from '../provider/provider';
|
|
4
|
-
import { generateObject
|
|
4
|
+
import { generateObject } from 'ai';
|
|
5
5
|
import PROMPT_GENERATE from './generate.txt';
|
|
6
6
|
import { SystemPrompt } from '../session/system';
|
|
7
7
|
import { Instance } from '../project/instance';
|
|
@@ -138,13 +138,14 @@ 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 `system`, not in `prompt`: the AI SDK
|
|
142
|
+
// warns that a system message inside the prompt enables prompt
|
|
143
|
+
// injection, and providers are free to treat it as ordinary content.
|
|
144
|
+
system: system.map((content) => ({
|
|
145
|
+
role: 'system' as const,
|
|
146
|
+
content,
|
|
147
|
+
})),
|
|
141
148
|
prompt: [
|
|
142
|
-
...system.map(
|
|
143
|
-
(item): ModelMessage => ({
|
|
144
|
-
role: 'system',
|
|
145
|
-
content: item,
|
|
146
|
-
})
|
|
147
|
-
),
|
|
148
149
|
{
|
|
149
150
|
role: 'user',
|
|
150
151
|
content: `Create an agent configuration based on this request: \"${input.description}\".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(', ')}\n Return ONLY the JSON object, no other text, do not wrap in backticks`,
|
|
@@ -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
|
}
|
package/src/project/instance.ts
CHANGED
|
@@ -58,6 +58,7 @@ export const Instance = {
|
|
|
58
58
|
async dispose() {
|
|
59
59
|
Log.Default.info('disposing instance', { directory: Instance.directory });
|
|
60
60
|
await State.dispose(Instance.directory);
|
|
61
|
+
await Project.prune();
|
|
61
62
|
},
|
|
62
63
|
async disposeAll() {
|
|
63
64
|
Log.Default.info('disposing all instances');
|
package/src/project/project.ts
CHANGED
|
@@ -4,9 +4,12 @@ import path from 'path';
|
|
|
4
4
|
import { $ } from 'bun';
|
|
5
5
|
import { Storage } from '../storage/storage';
|
|
6
6
|
import { Log } from '../util/log';
|
|
7
|
+
import fs from 'node:fs/promises';
|
|
8
|
+
import { Global } from '../global';
|
|
7
9
|
|
|
8
10
|
export namespace Project {
|
|
9
11
|
const log = Log.create({ service: 'project' });
|
|
12
|
+
const recentSnapshotAge = 15 * 60_000;
|
|
10
13
|
export const Info = z
|
|
11
14
|
.object({
|
|
12
15
|
id: z.string(),
|
|
@@ -24,6 +27,7 @@ export namespace Project {
|
|
|
24
27
|
|
|
25
28
|
export async function fromDirectory(directory: string) {
|
|
26
29
|
log.info(() => ({ message: 'fromDirectory', directory }));
|
|
30
|
+
await prune();
|
|
27
31
|
const matches = Filesystem.up({ targets: ['.git'], start: directory });
|
|
28
32
|
const git = await matches.next().then((x) => x.value);
|
|
29
33
|
await matches.return();
|
|
@@ -101,4 +105,66 @@ export namespace Project {
|
|
|
101
105
|
const keys = await Storage.list(['project']);
|
|
102
106
|
return await Promise.all(keys.map((x) => Storage.read<Info>(x)));
|
|
103
107
|
}
|
|
108
|
+
|
|
109
|
+
export async function prune() {
|
|
110
|
+
const root = path.join(Global.Path.data, 'snapshot');
|
|
111
|
+
const entries = await fs
|
|
112
|
+
.readdir(root, { withFileTypes: true })
|
|
113
|
+
.catch(() => []);
|
|
114
|
+
const snapshots: { id: string; path: string; modified: number }[] = [];
|
|
115
|
+
for (const entry of entries) {
|
|
116
|
+
if (!entry.isDirectory()) continue;
|
|
117
|
+
const snapshot = path.join(root, entry.name);
|
|
118
|
+
const modified = await fs
|
|
119
|
+
.stat(snapshot)
|
|
120
|
+
.then((stat) => stat.mtimeMs)
|
|
121
|
+
.catch(() => undefined);
|
|
122
|
+
if (modified === undefined) continue;
|
|
123
|
+
snapshots.push({ id: entry.name, path: snapshot, modified });
|
|
124
|
+
}
|
|
125
|
+
snapshots.sort((a, b) => b.modified - a.modified);
|
|
126
|
+
|
|
127
|
+
const newest = snapshots[0]?.id;
|
|
128
|
+
const recent = Date.now() - recentSnapshotAge;
|
|
129
|
+
for (const snapshot of snapshots) {
|
|
130
|
+
if (snapshot.id === newest || snapshot.modified >= recent) continue;
|
|
131
|
+
const project = await Storage.read<Info>(['project', snapshot.id]).catch(
|
|
132
|
+
() => undefined
|
|
133
|
+
);
|
|
134
|
+
const live = project
|
|
135
|
+
? await fs
|
|
136
|
+
.stat(project.worktree)
|
|
137
|
+
.then((stat) => stat.isDirectory())
|
|
138
|
+
.catch(() => false)
|
|
139
|
+
: false;
|
|
140
|
+
if (live) continue;
|
|
141
|
+
|
|
142
|
+
await remove(snapshot).catch((error) =>
|
|
143
|
+
log.warn(() => ({
|
|
144
|
+
message: 'failed to prune snapshot',
|
|
145
|
+
projectID: snapshot.id,
|
|
146
|
+
error: error instanceof Error ? error.message : String(error),
|
|
147
|
+
}))
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function remove(snapshot: { id: string; path: string }) {
|
|
153
|
+
await fs.rm(snapshot.path, { recursive: true, force: true });
|
|
154
|
+
for (const session of await Storage.list(['session', snapshot.id])) {
|
|
155
|
+
const sessionID = session.at(-1)!;
|
|
156
|
+
for (const message of await Storage.list(['message', sessionID])) {
|
|
157
|
+
const messageID = message.at(-1)!;
|
|
158
|
+
for (const part of await Storage.list(['part', messageID])) {
|
|
159
|
+
await Storage.remove(part);
|
|
160
|
+
}
|
|
161
|
+
await Storage.remove(message);
|
|
162
|
+
}
|
|
163
|
+
await Storage.remove(['session_diff', sessionID]);
|
|
164
|
+
await Storage.remove(['todo', sessionID]);
|
|
165
|
+
await Storage.remove(session);
|
|
166
|
+
}
|
|
167
|
+
await Storage.remove(['project', snapshot.id]);
|
|
168
|
+
log.info(() => ({ message: 'pruned snapshot', projectID: snapshot.id }));
|
|
169
|
+
}
|
|
104
170
|
}
|
package/src/session/summary.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Provider } from '../provider/provider';
|
|
|
2
2
|
import { fn } from '../util/fn';
|
|
3
3
|
import z from 'zod';
|
|
4
4
|
import { Session } from '.';
|
|
5
|
-
import { generateText
|
|
5
|
+
import { generateText } from 'ai';
|
|
6
6
|
import { MessageV2 } from './message-v2';
|
|
7
7
|
import { Identifier } from '../id/id';
|
|
8
8
|
import { Snapshot } from '../snapshot';
|
|
@@ -187,13 +187,14 @@ export namespace SessionSummary {
|
|
|
187
187
|
model.providerID,
|
|
188
188
|
{}
|
|
189
189
|
),
|
|
190
|
+
// The system prompt belongs in `system`, not in `messages`: the AI SDK
|
|
191
|
+
// warns that a system message inside `messages` enables prompt
|
|
192
|
+
// injection, and providers are free to treat it as ordinary content.
|
|
193
|
+
system: systemPrompts.map((content) => ({
|
|
194
|
+
role: 'system' as const,
|
|
195
|
+
content,
|
|
196
|
+
})),
|
|
190
197
|
messages: [
|
|
191
|
-
...systemPrompts.map(
|
|
192
|
-
(x): ModelMessage => ({
|
|
193
|
-
role: 'system',
|
|
194
|
-
content: x,
|
|
195
|
-
})
|
|
196
|
-
),
|
|
197
198
|
{
|
|
198
199
|
role: 'user' as const,
|
|
199
200
|
content: userContent,
|
package/src/snapshot/index.ts
CHANGED
|
@@ -30,6 +30,25 @@ export namespace Snapshot {
|
|
|
30
30
|
.nothrow();
|
|
31
31
|
log.info(() => ({ message: 'initialized' }));
|
|
32
32
|
}
|
|
33
|
+
const objects =
|
|
34
|
+
await $`git rev-parse --path-format=absolute --git-path objects`
|
|
35
|
+
.quiet()
|
|
36
|
+
.cwd(Instance.worktree)
|
|
37
|
+
.nothrow();
|
|
38
|
+
if (objects.exitCode === 0) {
|
|
39
|
+
const info = path.join(git, 'objects', 'info');
|
|
40
|
+
await fs.mkdir(info, { recursive: true });
|
|
41
|
+
await fs.writeFile(
|
|
42
|
+
path.join(info, 'alternates'),
|
|
43
|
+
`${objects.text().trim()}\n`
|
|
44
|
+
);
|
|
45
|
+
} else {
|
|
46
|
+
log.warn(() => ({
|
|
47
|
+
message: 'failed to resolve repository objects',
|
|
48
|
+
exitCode: objects.exitCode,
|
|
49
|
+
stderr: objects.stderr.toString(),
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
33
52
|
await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`
|
|
34
53
|
.quiet()
|
|
35
54
|
.cwd(Instance.directory)
|