@graphorin/agent 0.6.0 → 0.7.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/CHANGELOG.md +96 -0
- package/README.md +31 -11
- package/dist/errors/index.d.ts +17 -4
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +19 -3
- package/dist/errors/index.js.map +1 -1
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +153 -1930
- package/dist/factory.js.map +1 -1
- package/dist/fanout/index.d.ts +13 -1
- package/dist/fanout/index.d.ts.map +1 -1
- package/dist/fanout/index.js +13 -4
- package/dist/fanout/index.js.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -9
- package/dist/index.js.map +1 -1
- package/dist/lateral-leak/index.js +1 -1
- package/dist/package.js +6 -0
- package/dist/package.js.map +1 -0
- package/dist/run-state/index.d.ts +32 -6
- package/dist/run-state/index.d.ts.map +1 -1
- package/dist/run-state/index.js +46 -22
- package/dist/run-state/index.js.map +1 -1
- package/dist/runtime/agent-surface.js +122 -0
- package/dist/runtime/agent-surface.js.map +1 -0
- package/dist/runtime/agent-to-tool.d.ts +51 -0
- package/dist/runtime/agent-to-tool.d.ts.map +1 -0
- package/dist/runtime/agent-to-tool.js +145 -0
- package/dist/runtime/agent-to-tool.js.map +1 -0
- package/dist/runtime/approvals.js +0 -0
- package/dist/runtime/approvals.js.map +1 -0
- package/dist/runtime/dispatch.js +108 -0
- package/dist/runtime/dispatch.js.map +1 -0
- package/dist/runtime/executor-wiring.js +128 -0
- package/dist/runtime/executor-wiring.js.map +1 -0
- package/dist/runtime/fallback-chain.js +139 -0
- package/dist/runtime/fallback-chain.js.map +1 -0
- package/dist/runtime/handoff.js +307 -0
- package/dist/runtime/handoff.js.map +1 -0
- package/dist/runtime/messages.d.ts +22 -0
- package/dist/runtime/messages.d.ts.map +1 -0
- package/dist/runtime/messages.js +204 -0
- package/dist/runtime/messages.js.map +1 -0
- package/dist/runtime/provider-events.js +117 -0
- package/dist/runtime/provider-events.js.map +1 -0
- package/dist/runtime/run-compaction.js +210 -0
- package/dist/runtime/run-compaction.js.map +1 -0
- package/dist/runtime/run-finish.js +48 -0
- package/dist/runtime/run-finish.js.map +1 -0
- package/dist/runtime/run-gates.js +336 -0
- package/dist/runtime/run-gates.js.map +1 -0
- package/dist/runtime/run-init.js +81 -0
- package/dist/runtime/run-init.js.map +1 -0
- package/dist/runtime/run-input.js +46 -0
- package/dist/runtime/run-input.js.map +1 -0
- package/dist/runtime/step-catalogue.js +173 -0
- package/dist/runtime/step-catalogue.js.map +1 -0
- package/dist/runtime/tool-call-walk.js +189 -0
- package/dist/runtime/tool-call-walk.js.map +1 -0
- package/dist/runtime/tool-wiring.js +159 -0
- package/dist/runtime/tool-wiring.js.map +1 -0
- package/dist/tooling/adapters.js +1 -1
- package/dist/tooling/dataflow.js +1 -1
- package/dist/tooling/policy.js +2 -0
- package/dist/tooling/policy.js.map +1 -1
- package/dist/types.d.ts +63 -13
- package/dist/types.d.ts.map +1 -1
- package/package.json +20 -20
- package/src/errors/index.ts +320 -0
- package/src/evaluator-optimizer/index.ts +212 -0
- package/src/factory.ts +957 -0
- package/src/fallback/index.ts +108 -0
- package/src/fanout/index.ts +523 -0
- package/src/filters/index.ts +347 -0
- package/src/index.ts +180 -0
- package/src/internal/ids.ts +46 -0
- package/src/internal/usage-accumulator.ts +90 -0
- package/src/lateral-leak/causality-monitor.ts +221 -0
- package/src/lateral-leak/index.ts +35 -0
- package/src/lateral-leak/merge-guard.ts +151 -0
- package/src/lateral-leak/protocol-guard.ts +222 -0
- package/src/preferred-model/index.ts +210 -0
- package/src/progress/index.ts +238 -0
- package/src/run-state/index.ts +607 -0
- package/src/runtime/agent-surface.ts +218 -0
- package/src/runtime/agent-to-tool.ts +323 -0
- package/src/runtime/approvals.ts +0 -0
- package/src/runtime/dispatch.ts +183 -0
- package/src/runtime/executor-wiring.ts +331 -0
- package/src/runtime/fallback-chain.ts +250 -0
- package/src/runtime/handoff.ts +428 -0
- package/src/runtime/messages.ts +309 -0
- package/src/runtime/provider-events.ts +175 -0
- package/src/runtime/run-compaction.ts +288 -0
- package/src/runtime/run-finish.ts +93 -0
- package/src/runtime/run-gates.ts +419 -0
- package/src/runtime/run-init.ts +169 -0
- package/src/runtime/run-input.ts +102 -0
- package/src/runtime/step-catalogue.ts +338 -0
- package/src/runtime/tool-call-walk.ts +301 -0
- package/src/runtime/tool-wiring.ts +218 -0
- package/src/testing/replay-provider.ts +121 -0
- package/src/tooling/adapters.ts +403 -0
- package/src/tooling/catalogue.ts +36 -0
- package/src/tooling/dataflow.ts +171 -0
- package/src/tooling/plan.ts +123 -0
- package/src/tooling/policy.ts +67 -0
- package/src/tooling/registry-build.ts +191 -0
- package/src/types.ts +696 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-tool / per-agent preferred-model resolution. Pure functions
|
|
3
|
+
* consulted by the agent loop AFTER the model has decided which
|
|
4
|
+
* tool(s) to call but BEFORE `provider.stream(...)` is invoked.
|
|
5
|
+
*
|
|
6
|
+
* The four-step precedence ladder (highest wins):
|
|
7
|
+
*
|
|
8
|
+
* 1. `prepareStep({ provider })` - the operator's explicit per-step
|
|
9
|
+
* override always wins.
|
|
10
|
+
* 2. `Tool.preferredModel` - the tool author's per-tool hint.
|
|
11
|
+
* Only the tools the model actually CALLED on the previous step
|
|
12
|
+
* are consulted (AG-15) - an advertised-but-uncalled hint never
|
|
13
|
+
* escalates the run. Multi-tool ties resolve to the highest cost
|
|
14
|
+
* tier (`'smart' > 'balanced' > 'fast'`; explicit `ModelSpec` is
|
|
15
|
+
* treated as the highest tier).
|
|
16
|
+
* 3. `Agent.preferredModel?` - the per-agent default.
|
|
17
|
+
* 4. `Agent` default `provider` - the v0.1-alpha behaviour.
|
|
18
|
+
*
|
|
19
|
+
* Cost-tier resolution against `Agent.modelTierMap` is documented
|
|
20
|
+
* as a hint: when the requested tier is unmapped, the resolver
|
|
21
|
+
* falls through to the next precedence step rather than throwing.
|
|
22
|
+
*
|
|
23
|
+
* @packageDocumentation
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { ModelHint, ModelSpec, Provider } from '@graphorin/core';
|
|
27
|
+
|
|
28
|
+
const TIER_ORDER: Record<ModelHint, number> = { fast: 0, balanced: 1, smart: 2 };
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Result returned by {@link resolvePreferredModel}.
|
|
32
|
+
*
|
|
33
|
+
* @stable
|
|
34
|
+
*/
|
|
35
|
+
export interface PreferredModelResolution {
|
|
36
|
+
readonly resolvedProvider: Provider;
|
|
37
|
+
readonly resolvedModelId: string;
|
|
38
|
+
readonly source: 'prepare-step' | 'tier-map' | 'spec' | 'agent-preferred' | 'fallthrough-default';
|
|
39
|
+
readonly hintApplied?: ModelHint;
|
|
40
|
+
readonly fallthroughReason?:
|
|
41
|
+
| 'tier-not-mapped'
|
|
42
|
+
| 'provider-unavailable'
|
|
43
|
+
| 'override-takes-precedence';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Pure inputs to {@link resolvePreferredModel}.
|
|
48
|
+
*
|
|
49
|
+
* @stable
|
|
50
|
+
*/
|
|
51
|
+
export interface ResolvePreferredModelInput {
|
|
52
|
+
readonly prepareStepProvider?: Provider;
|
|
53
|
+
readonly toolPreferredModels: ReadonlyArray<ModelHint | ModelSpec | undefined>;
|
|
54
|
+
readonly agentPreferredModel?: ModelHint | ModelSpec;
|
|
55
|
+
readonly agentDefaultProvider: Provider;
|
|
56
|
+
readonly modelTierMap?: Partial<Record<ModelHint, ModelSpec>>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isModelHint(value: ModelHint | ModelSpec | undefined): value is ModelHint {
|
|
60
|
+
return value === 'fast' || value === 'balanced' || value === 'smart';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function specToProvider(spec: ModelSpec): Provider {
|
|
64
|
+
if ('provider' in spec) return spec.provider as Provider;
|
|
65
|
+
return spec as Provider;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function modelIdFromSpec(spec: ModelSpec): string {
|
|
69
|
+
if ('provider' in spec) return spec.model;
|
|
70
|
+
return (spec as Provider).modelId;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Pick the highest-cost tier across the supplied per-tool hints.
|
|
75
|
+
* Explicit `ModelSpec` entries are treated as the highest tier
|
|
76
|
+
* (`'smart'`) for tie-breaking - the conservative-correctness rule
|
|
77
|
+
* documented in DEC-169 / suggested ADR-057.
|
|
78
|
+
*
|
|
79
|
+
* Returns the picked hint together with the original `ModelSpec`
|
|
80
|
+
* (when an explicit spec won the tie-break).
|
|
81
|
+
*
|
|
82
|
+
* @stable
|
|
83
|
+
*/
|
|
84
|
+
export function pickTopTierAcrossTools(
|
|
85
|
+
hints: ReadonlyArray<ModelHint | ModelSpec | undefined>,
|
|
86
|
+
): { readonly hint: ModelHint; readonly spec?: ModelSpec } | undefined {
|
|
87
|
+
let bestRank = -1;
|
|
88
|
+
let bestSpec: ModelSpec | undefined;
|
|
89
|
+
let bestHint: ModelHint | undefined;
|
|
90
|
+
for (const h of hints) {
|
|
91
|
+
if (h === undefined) continue;
|
|
92
|
+
if (isModelHint(h)) {
|
|
93
|
+
const rank = TIER_ORDER[h];
|
|
94
|
+
if (rank > bestRank) {
|
|
95
|
+
bestRank = rank;
|
|
96
|
+
bestSpec = undefined;
|
|
97
|
+
bestHint = h;
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
// Explicit `ModelSpec` -> treat as the highest tier.
|
|
101
|
+
const rank = TIER_ORDER.smart;
|
|
102
|
+
if (rank >= bestRank) {
|
|
103
|
+
bestRank = rank;
|
|
104
|
+
bestSpec = h;
|
|
105
|
+
bestHint = 'smart';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (bestHint === undefined) return undefined;
|
|
110
|
+
if (bestSpec !== undefined) return { hint: bestHint, spec: bestSpec };
|
|
111
|
+
return { hint: bestHint };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Walk the precedence ladder and return the resolved provider for a
|
|
116
|
+
* single agent step. Pure function - no side effects.
|
|
117
|
+
*
|
|
118
|
+
* @stable
|
|
119
|
+
*/
|
|
120
|
+
export function resolvePreferredModel(input: ResolvePreferredModelInput): PreferredModelResolution {
|
|
121
|
+
if (input.prepareStepProvider !== undefined) {
|
|
122
|
+
return {
|
|
123
|
+
resolvedProvider: input.prepareStepProvider,
|
|
124
|
+
resolvedModelId: input.prepareStepProvider.modelId,
|
|
125
|
+
source: 'prepare-step',
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const top = pickTopTierAcrossTools(input.toolPreferredModels);
|
|
129
|
+
if (top !== undefined) {
|
|
130
|
+
if (top.spec !== undefined) {
|
|
131
|
+
return {
|
|
132
|
+
resolvedProvider: specToProvider(top.spec),
|
|
133
|
+
resolvedModelId: modelIdFromSpec(top.spec),
|
|
134
|
+
source: 'spec',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const mapped = input.modelTierMap?.[top.hint];
|
|
138
|
+
if (mapped !== undefined) {
|
|
139
|
+
return {
|
|
140
|
+
resolvedProvider: specToProvider(mapped),
|
|
141
|
+
resolvedModelId: modelIdFromSpec(mapped),
|
|
142
|
+
source: 'tier-map',
|
|
143
|
+
hintApplied: top.hint,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
// Fall through to agent-preferred / default.
|
|
147
|
+
const agent = input.agentPreferredModel;
|
|
148
|
+
if (agent !== undefined) {
|
|
149
|
+
if (isModelHint(agent)) {
|
|
150
|
+
const agentMapped = input.modelTierMap?.[agent];
|
|
151
|
+
if (agentMapped !== undefined) {
|
|
152
|
+
return {
|
|
153
|
+
resolvedProvider: specToProvider(agentMapped),
|
|
154
|
+
resolvedModelId: modelIdFromSpec(agentMapped),
|
|
155
|
+
source: 'agent-preferred',
|
|
156
|
+
hintApplied: agent,
|
|
157
|
+
fallthroughReason: 'tier-not-mapped',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
return {
|
|
162
|
+
resolvedProvider: specToProvider(agent),
|
|
163
|
+
resolvedModelId: modelIdFromSpec(agent),
|
|
164
|
+
source: 'agent-preferred',
|
|
165
|
+
fallthroughReason: 'tier-not-mapped',
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
resolvedProvider: input.agentDefaultProvider,
|
|
171
|
+
resolvedModelId: input.agentDefaultProvider.modelId,
|
|
172
|
+
source: 'fallthrough-default',
|
|
173
|
+
hintApplied: top.hint,
|
|
174
|
+
fallthroughReason: 'tier-not-mapped',
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
// No per-tool hints. Honour `Agent.preferredModel` next.
|
|
178
|
+
const agent = input.agentPreferredModel;
|
|
179
|
+
if (agent !== undefined) {
|
|
180
|
+
if (isModelHint(agent)) {
|
|
181
|
+
const mapped = input.modelTierMap?.[agent];
|
|
182
|
+
if (mapped !== undefined) {
|
|
183
|
+
return {
|
|
184
|
+
resolvedProvider: specToProvider(mapped),
|
|
185
|
+
resolvedModelId: modelIdFromSpec(mapped),
|
|
186
|
+
source: 'agent-preferred',
|
|
187
|
+
hintApplied: agent,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
// Agent-default tier not mapped: fall through.
|
|
191
|
+
return {
|
|
192
|
+
resolvedProvider: input.agentDefaultProvider,
|
|
193
|
+
resolvedModelId: input.agentDefaultProvider.modelId,
|
|
194
|
+
source: 'fallthrough-default',
|
|
195
|
+
hintApplied: agent,
|
|
196
|
+
fallthroughReason: 'tier-not-mapped',
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
resolvedProvider: specToProvider(agent),
|
|
201
|
+
resolvedModelId: modelIdFromSpec(agent),
|
|
202
|
+
source: 'agent-preferred',
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
resolvedProvider: input.agentDefaultProvider,
|
|
207
|
+
resolvedModelId: input.agentDefaultProvider.modelId,
|
|
208
|
+
source: 'fallthrough-default',
|
|
209
|
+
};
|
|
210
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured progress-artifact IO. Persists UTF-8 text artifacts
|
|
3
|
+
* under `<artifactRoot>/<runId>/progress/<role>.<seqPadded>.txt`
|
|
4
|
+
* via atomic-write `.tmp + rename` discipline.
|
|
5
|
+
*
|
|
6
|
+
* Cross-session continuity flow:
|
|
7
|
+
*
|
|
8
|
+
* 1. The current agent calls `agent.progress.write(content)` -
|
|
9
|
+
* runtime persists the file and queues the
|
|
10
|
+
* `agent.progress.written` event (drained into the active or
|
|
11
|
+
* next consumed stream).
|
|
12
|
+
* 2. A sibling / future agent calls
|
|
13
|
+
* `agent.progress.read({ runId: priorRunId })` - runtime
|
|
14
|
+
* discovers existing files (no implicit auto-discovery; the
|
|
15
|
+
* operator must supply the `runId` cursor).
|
|
16
|
+
*
|
|
17
|
+
* Auto-discovery across runs is deferred to v0.2.
|
|
18
|
+
*
|
|
19
|
+
* @packageDocumentation
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { createHash } from 'node:crypto';
|
|
23
|
+
import { mkdir, readdir, readFile, rename, stat, unlink, writeFile } from 'node:fs/promises';
|
|
24
|
+
import { tmpdir } from 'node:os';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import type { ProgressArtifactRef, Sensitivity } from '@graphorin/core';
|
|
27
|
+
import { ProgressWriteError } from '../errors/index.js';
|
|
28
|
+
|
|
29
|
+
const DEFAULT_ARTIFACT_DIR = 'graphorin-progress';
|
|
30
|
+
const SEQ_PAD_WIDTH = 3;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Optional configuration accepted by {@link createProgressIO}.
|
|
34
|
+
*
|
|
35
|
+
* @stable
|
|
36
|
+
*/
|
|
37
|
+
export interface ProgressIOConfig {
|
|
38
|
+
/** Filesystem root under which `<runId>/progress/<role>.<seq>.txt` files live. */
|
|
39
|
+
readonly artifactRoot?: string;
|
|
40
|
+
/** Default `Sensitivity` applied when the caller does not override. */
|
|
41
|
+
readonly defaultSensitivity?: Sensitivity;
|
|
42
|
+
/** Optional redaction transform applied to content before write. */
|
|
43
|
+
readonly redact?: (content: string) => string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Per-call options for {@link ProgressIO.write}.
|
|
48
|
+
*
|
|
49
|
+
* @stable
|
|
50
|
+
*/
|
|
51
|
+
export interface ProgressWriteOptions {
|
|
52
|
+
readonly role?: string;
|
|
53
|
+
/** Explicit sequence number; default auto-increments per `(runId, role)`. */
|
|
54
|
+
readonly seq?: number;
|
|
55
|
+
readonly sensitivity?: Sensitivity;
|
|
56
|
+
readonly tags?: ReadonlyArray<string>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Per-call options for {@link ProgressIO.read}.
|
|
61
|
+
*
|
|
62
|
+
* @stable
|
|
63
|
+
*/
|
|
64
|
+
export interface ProgressReadOptions {
|
|
65
|
+
readonly runId?: string;
|
|
66
|
+
readonly role?: string;
|
|
67
|
+
/** Skip artifacts whose `seq <= sinceSeq`. */
|
|
68
|
+
readonly sinceSeq?: number;
|
|
69
|
+
/** Default `100`. */
|
|
70
|
+
readonly maxArtifacts?: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Public surface returned by {@link createProgressIO}. Used by the
|
|
75
|
+
* agent runtime to back `agent.progress.write / read`.
|
|
76
|
+
*
|
|
77
|
+
* @stable
|
|
78
|
+
*/
|
|
79
|
+
export interface ProgressIO {
|
|
80
|
+
write(
|
|
81
|
+
runId: string,
|
|
82
|
+
content: string,
|
|
83
|
+
options?: ProgressWriteOptions,
|
|
84
|
+
): Promise<ProgressArtifactRef>;
|
|
85
|
+
read(
|
|
86
|
+
currentRunId: string,
|
|
87
|
+
options?: ProgressReadOptions,
|
|
88
|
+
): Promise<ReadonlyArray<ProgressArtifactRef>>;
|
|
89
|
+
rootFor(runId: string): string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface InternalSeqState {
|
|
93
|
+
next: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const seqWidth = (n: number): string => String(n).padStart(SEQ_PAD_WIDTH, '0');
|
|
97
|
+
|
|
98
|
+
const sha256Hex = (input: string): string =>
|
|
99
|
+
createHash('sha256').update(input, 'utf8').digest('hex');
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Build a {@link ProgressIO} bound to a particular artifact root.
|
|
103
|
+
*
|
|
104
|
+
* @stable
|
|
105
|
+
*/
|
|
106
|
+
export function createProgressIO(config: ProgressIOConfig = {}): ProgressIO {
|
|
107
|
+
const root = config.artifactRoot ?? join(tmpdir(), DEFAULT_ARTIFACT_DIR);
|
|
108
|
+
const defaultSensitivity: Sensitivity = config.defaultSensitivity ?? 'internal';
|
|
109
|
+
const redact: (s: string) => string = config.redact ?? ((s) => s);
|
|
110
|
+
const seqState = new Map<string, InternalSeqState>();
|
|
111
|
+
|
|
112
|
+
const directoryFor = (runId: string): string => join(root, runId, 'progress');
|
|
113
|
+
|
|
114
|
+
const seqKey = (runId: string, role: string): string => `${runId}::${role}`;
|
|
115
|
+
|
|
116
|
+
const ensureSeqState = async (runId: string, role: string): Promise<InternalSeqState> => {
|
|
117
|
+
const key = seqKey(runId, role);
|
|
118
|
+
let state = seqState.get(key);
|
|
119
|
+
if (state !== undefined) return state;
|
|
120
|
+
state = { next: 1 };
|
|
121
|
+
try {
|
|
122
|
+
const dir = directoryFor(runId);
|
|
123
|
+
const entries = await readdir(dir);
|
|
124
|
+
let max = 0;
|
|
125
|
+
const prefix = `${role}.`;
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
if (!entry.startsWith(prefix) || !entry.endsWith('.txt')) continue;
|
|
128
|
+
const seqStr = entry.slice(prefix.length, entry.length - 4);
|
|
129
|
+
const n = Number(seqStr);
|
|
130
|
+
if (Number.isFinite(n) && n > max) max = n;
|
|
131
|
+
}
|
|
132
|
+
state.next = max + 1;
|
|
133
|
+
} catch {
|
|
134
|
+
// Directory does not exist yet - first write will create it.
|
|
135
|
+
}
|
|
136
|
+
seqState.set(key, state);
|
|
137
|
+
return state;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const write = async (
|
|
141
|
+
runId: string,
|
|
142
|
+
rawContent: string,
|
|
143
|
+
options: ProgressWriteOptions = {},
|
|
144
|
+
): Promise<ProgressArtifactRef> => {
|
|
145
|
+
const role = options.role ?? 'agent';
|
|
146
|
+
const sensitivity = options.sensitivity ?? defaultSensitivity;
|
|
147
|
+
const tags = options.tags ?? [];
|
|
148
|
+
const seqSource = await ensureSeqState(runId, role);
|
|
149
|
+
const seq = options.seq ?? seqSource.next;
|
|
150
|
+
seqSource.next = Math.max(seqSource.next, seq + 1);
|
|
151
|
+
|
|
152
|
+
const content = redact(rawContent);
|
|
153
|
+
const dir = directoryFor(runId);
|
|
154
|
+
const filename = `${role}.${seqWidth(seq)}.txt`;
|
|
155
|
+
const finalPath = join(dir, filename);
|
|
156
|
+
const tmpPath = `${finalPath}.tmp`;
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
await mkdir(dir, { recursive: true });
|
|
160
|
+
await writeFile(tmpPath, content, { encoding: 'utf8' });
|
|
161
|
+
await rename(tmpPath, finalPath);
|
|
162
|
+
} catch (cause) {
|
|
163
|
+
try {
|
|
164
|
+
await unlink(tmpPath);
|
|
165
|
+
} catch {
|
|
166
|
+
// Best-effort cleanup; the write error is the operator-facing signal.
|
|
167
|
+
}
|
|
168
|
+
throw new ProgressWriteError(finalPath, cause);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const stats = await stat(finalPath);
|
|
172
|
+
const sha256 = sha256Hex(content);
|
|
173
|
+
return {
|
|
174
|
+
path: finalPath,
|
|
175
|
+
role,
|
|
176
|
+
seq,
|
|
177
|
+
sizeBytes: stats.size,
|
|
178
|
+
sensitivity,
|
|
179
|
+
...(tags.length > 0 ? { tags } : {}),
|
|
180
|
+
writtenAtIso: new Date().toISOString(),
|
|
181
|
+
sha256,
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const read = async (
|
|
186
|
+
currentRunId: string,
|
|
187
|
+
options: ProgressReadOptions = {},
|
|
188
|
+
): Promise<ReadonlyArray<ProgressArtifactRef>> => {
|
|
189
|
+
const runId = options.runId ?? currentRunId;
|
|
190
|
+
const max = options.maxArtifacts ?? 100;
|
|
191
|
+
const sinceSeq = options.sinceSeq ?? 0;
|
|
192
|
+
const dir = directoryFor(runId);
|
|
193
|
+
let entries: string[];
|
|
194
|
+
try {
|
|
195
|
+
entries = await readdir(dir);
|
|
196
|
+
} catch {
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
const out: ProgressArtifactRef[] = [];
|
|
200
|
+
const role = options.role;
|
|
201
|
+
for (const entry of entries) {
|
|
202
|
+
if (!entry.endsWith('.txt')) continue;
|
|
203
|
+
const dot = entry.indexOf('.');
|
|
204
|
+
if (dot < 0) continue;
|
|
205
|
+
const entryRole = entry.slice(0, dot);
|
|
206
|
+
const seqStr = entry.slice(dot + 1, entry.length - 4);
|
|
207
|
+
const seq = Number(seqStr);
|
|
208
|
+
if (!Number.isFinite(seq)) continue;
|
|
209
|
+
if (role !== undefined && entryRole !== role) continue;
|
|
210
|
+
if (seq <= sinceSeq) continue;
|
|
211
|
+
const fullPath = join(dir, entry);
|
|
212
|
+
const stats = await stat(fullPath);
|
|
213
|
+
const content = await readFile(fullPath, { encoding: 'utf8' });
|
|
214
|
+
const sha256 = sha256Hex(content);
|
|
215
|
+
out.push({
|
|
216
|
+
path: fullPath,
|
|
217
|
+
role: entryRole,
|
|
218
|
+
seq,
|
|
219
|
+
sizeBytes: stats.size,
|
|
220
|
+
sensitivity: 'internal',
|
|
221
|
+
writtenAtIso: stats.mtime.toISOString(),
|
|
222
|
+
sha256,
|
|
223
|
+
});
|
|
224
|
+
if (out.length >= max) break;
|
|
225
|
+
}
|
|
226
|
+
out.sort((a, b) => {
|
|
227
|
+
if (a.role !== b.role) return a.role.localeCompare(b.role);
|
|
228
|
+
return a.seq - b.seq;
|
|
229
|
+
});
|
|
230
|
+
return out;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
write,
|
|
235
|
+
read,
|
|
236
|
+
rootFor: (runId) => directoryFor(runId),
|
|
237
|
+
};
|
|
238
|
+
}
|