@aiwg/cli 2026.7.20 → 2026.7.21
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 +4 -4
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/browser-export.js +7 -0
- package/dist/src/artifacts/citation-parser.js +96 -35
- package/dist/src/artifacts/index-builder.js +54 -17
- package/dist/src/artifacts/state-transfer.js +27 -0
- package/dist/src/artifacts/stats.js +8 -0
- package/dist/src/cli/cli-extension-loader.js +73 -0
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/sessions.js +966 -0
- package/dist/src/cli/handlers/skill-lint.js +49 -45
- package/dist/src/cli/handlers/use.js +143 -60
- package/dist/src/cli/handlers/utilities.js +22 -8
- package/dist/src/cli/skill-usage.js +146 -24
- package/dist/src/extensions/commands/definitions.js +29 -0
- package/dist/src/extensions/manifest.js +29 -0
- package/dist/src/sessions/adapters/claude.js +357 -0
- package/dist/src/sessions/adapters/codex.js +521 -0
- package/dist/src/sessions/adapters/copilot.js +226 -0
- package/dist/src/sessions/adapters/cursor.js +372 -0
- package/dist/src/sessions/adapters/factory.js +345 -0
- package/dist/src/sessions/adapters/generic.js +225 -0
- package/dist/src/sessions/adapters/hermes.js +341 -0
- package/dist/src/sessions/adapters/openclaw.js +381 -0
- package/dist/src/sessions/adapters/opencode.js +454 -0
- package/dist/src/sessions/adapters/openhuman.js +315 -0
- package/dist/src/sessions/adapters/warp.js +160 -0
- package/dist/src/sessions/adapters/windsurf.js +212 -0
- package/dist/src/sessions/candidates.js +210 -0
- package/dist/src/sessions/contracts.js +310 -0
- package/dist/src/sessions/discovery.js +51 -0
- package/dist/src/sessions/fixtures.js +12 -0
- package/dist/src/sessions/importer.js +315 -0
- package/dist/src/sessions/index.js +25 -0
- package/dist/src/sessions/knowledge-shard.js +61 -0
- package/dist/src/sessions/optional-backends.js +238 -0
- package/dist/src/sessions/policy.js +192 -0
- package/dist/src/sessions/ports.js +2 -0
- package/dist/src/sessions/promotion.js +367 -0
- package/dist/src/sessions/readers.js +176 -0
- package/dist/src/sessions/repository.js +1551 -0
- package/dist/src/skills/adapters/agent-skills.js +59 -0
- package/dist/src/skills/adapters/local.js +19 -1
- package/dist/src/skills/agent-skills.js +249 -0
- package/dist/src/skills/cli.js +463 -7
- package/dist/src/skills/deployer.js +554 -0
- package/dist/src/skills/doctor.js +105 -0
- package/dist/src/skills/exporter.js +382 -0
- package/dist/src/skills/importer.js +921 -0
- package/dist/src/skills/registry.js +19 -0
- package/dist/src/skills/validator.js +323 -0
- package/package.json +2 -2
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { opendir } from 'node:fs/promises';
|
|
2
|
+
import { basename, extname, resolve } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { SessionContractError, assertSupportedSchemaMajor, } from '../contracts.js';
|
|
5
|
+
import { readBoundedJsonLines, streamBoundedJsonLines, } from '../readers.js';
|
|
6
|
+
export const FACTORY_ADAPTER_VERSION = '1.0.0';
|
|
7
|
+
export const FACTORY_SOURCE_SCHEMA_VERSION = '1.0.0';
|
|
8
|
+
const FactoryRecordSchema = z.object({
|
|
9
|
+
schemaVersion: z.string().optional(),
|
|
10
|
+
type: z.string().min(1),
|
|
11
|
+
subtype: z.string().optional(),
|
|
12
|
+
uuid: z.string().min(1).optional(),
|
|
13
|
+
id: z.string().min(1).optional(),
|
|
14
|
+
parentUuid: z.string().nullable().optional(),
|
|
15
|
+
sessionId: z.string().min(1).optional(),
|
|
16
|
+
session_id: z.string().min(1).optional(),
|
|
17
|
+
timestamp: z.union([z.string(), z.number()]).optional(),
|
|
18
|
+
createdAt: z.number().optional(),
|
|
19
|
+
updatedAt: z.number().optional(),
|
|
20
|
+
cwd: z.string().optional(),
|
|
21
|
+
version: z.string().optional(),
|
|
22
|
+
message: z.object({
|
|
23
|
+
id: z.string().optional(),
|
|
24
|
+
role: z.string().optional(),
|
|
25
|
+
content: z.union([z.string(), z.array(z.unknown())]).optional(),
|
|
26
|
+
}).passthrough().optional(),
|
|
27
|
+
content: z.union([z.string(), z.array(z.unknown())]).optional(),
|
|
28
|
+
sessionSettings: z.record(z.unknown()).optional(),
|
|
29
|
+
settings: z.record(z.unknown()).optional(),
|
|
30
|
+
status: z.string().optional(),
|
|
31
|
+
}).passthrough();
|
|
32
|
+
export class FactorySessionAdapter {
|
|
33
|
+
limits;
|
|
34
|
+
transports;
|
|
35
|
+
discoveryLimits;
|
|
36
|
+
provider = 'factory';
|
|
37
|
+
adapterVersion = FACTORY_ADAPTER_VERSION;
|
|
38
|
+
disposition = 'implemented';
|
|
39
|
+
supportedOperations = ['discover', 'inspect', 'stream'];
|
|
40
|
+
acquisitionModes = ['jsonl', 'api'];
|
|
41
|
+
constructor(limits, transports = [], discoveryLimits = { maxDepth: 8, maxFiles: 10_000 }) {
|
|
42
|
+
this.limits = limits;
|
|
43
|
+
this.transports = transports;
|
|
44
|
+
this.discoveryLimits = discoveryLimits;
|
|
45
|
+
}
|
|
46
|
+
async *discover(scope) {
|
|
47
|
+
if (scope.allowedRoots.length === 0) {
|
|
48
|
+
throw new SessionContractError('SOURCE_NOT_AUTHORIZED', 'Factory discovery requires an explicitly authorized projects root');
|
|
49
|
+
}
|
|
50
|
+
let emitted = 0;
|
|
51
|
+
for (const root of [...scope.allowedRoots].sort()) {
|
|
52
|
+
for await (const locator of discoverJsonl(resolve(root), this.discoveryLimits.maxDepth)) {
|
|
53
|
+
if (++emitted > this.discoveryLimits.maxFiles) {
|
|
54
|
+
throw new SessionContractError('RESOURCE_LIMIT_EXCEEDED', 'Factory source discovery exceeded the authorized file limit');
|
|
55
|
+
}
|
|
56
|
+
yield { provider: 'factory', locator, locatorClass: 'factory-droid-jsonl' };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async inspect(source) {
|
|
61
|
+
const parsed = await this.readSource(source);
|
|
62
|
+
return {
|
|
63
|
+
sourceSchemaVersion: parsed.schemaVersion,
|
|
64
|
+
consistency: parsed.consistency,
|
|
65
|
+
operationalState: 'available',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async *stream(source, cursor) {
|
|
69
|
+
if (source.locatorClass === 'factory-droid-jsonl') {
|
|
70
|
+
const input = await streamBoundedJsonLines({
|
|
71
|
+
selectedPath: source.locator,
|
|
72
|
+
allowedRoots: source.authorizedScope.allowedRoots,
|
|
73
|
+
}, { consistency: 'provisional', limits: this.limits });
|
|
74
|
+
const start = parseCursor(cursor?.value);
|
|
75
|
+
let outputIndex = 0;
|
|
76
|
+
let establishedId;
|
|
77
|
+
let schemaVersion = null;
|
|
78
|
+
let sawRecord = false;
|
|
79
|
+
for await (const line of input) {
|
|
80
|
+
sawRecord = true;
|
|
81
|
+
const parsed = FactoryRecordSchema.safeParse(line.value);
|
|
82
|
+
if (!parsed.success) {
|
|
83
|
+
throw new SessionContractError('MALFORMED_SOURCE', 'Factory session record is malformed');
|
|
84
|
+
}
|
|
85
|
+
const value = parsed.data;
|
|
86
|
+
const currentSchema = value.schemaVersion ?? FACTORY_SOURCE_SCHEMA_VERSION;
|
|
87
|
+
if (schemaVersion && schemaVersion !== currentSchema) {
|
|
88
|
+
throw new SessionContractError('SCHEMA_DRIFT', 'mixed Factory source schemas');
|
|
89
|
+
}
|
|
90
|
+
schemaVersion = currentSchema;
|
|
91
|
+
assertSupportedSchemaMajor(schemaVersion);
|
|
92
|
+
const filenameId = basename(source.locator, extname(source.locator));
|
|
93
|
+
const currentId = value.sessionId ?? value.session_id ?? establishedId ?? filenameId;
|
|
94
|
+
if (establishedId && establishedId !== currentId) {
|
|
95
|
+
throw new SessionContractError('DUPLICATE_NATIVE_ID', 'Factory source changes session identity');
|
|
96
|
+
}
|
|
97
|
+
establishedId = currentId;
|
|
98
|
+
for (const record of normalize([line], source).records) {
|
|
99
|
+
if (outputIndex++ >= start)
|
|
100
|
+
yield record;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (!sawRecord && !input.incompleteTail) {
|
|
104
|
+
throw new SessionContractError('MALFORMED_SOURCE', 'Factory session source is empty');
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const parsed = await this.readSource(source);
|
|
109
|
+
const start = parseCursor(cursor?.value);
|
|
110
|
+
for (const record of parsed.records.slice(start))
|
|
111
|
+
yield record;
|
|
112
|
+
}
|
|
113
|
+
async readSource(source) {
|
|
114
|
+
let input;
|
|
115
|
+
let incompleteTail = false;
|
|
116
|
+
if (source.locatorClass === 'factory-droid-jsonl') {
|
|
117
|
+
const result = await readBoundedJsonLines({
|
|
118
|
+
selectedPath: source.locator,
|
|
119
|
+
allowedRoots: source.authorizedScope.allowedRoots,
|
|
120
|
+
}, { consistency: 'provisional', limits: this.limits });
|
|
121
|
+
input = result.records;
|
|
122
|
+
incompleteTail = result.incompleteTail;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
input = await this.remoteRecords(source);
|
|
126
|
+
}
|
|
127
|
+
if (input.length === 0 && !incompleteTail) {
|
|
128
|
+
throw new SessionContractError('MALFORMED_SOURCE', 'Factory session source is empty');
|
|
129
|
+
}
|
|
130
|
+
const schemaVersion = declaredVersion(input);
|
|
131
|
+
assertSupportedSchemaMajor(schemaVersion);
|
|
132
|
+
const normalized = normalize(input, source);
|
|
133
|
+
return {
|
|
134
|
+
schemaVersion,
|
|
135
|
+
consistency: normalized.complete && !incompleteTail ? 'complete' : 'provisional',
|
|
136
|
+
records: normalized.records,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
async remoteRecords(source) {
|
|
140
|
+
const kind = source.locatorClass === 'factory-sessions-api'
|
|
141
|
+
? 'sessions-api'
|
|
142
|
+
: source.locatorClass === 'factory-exec-stream'
|
|
143
|
+
? 'droid-exec'
|
|
144
|
+
: undefined;
|
|
145
|
+
if (!kind)
|
|
146
|
+
throw new SessionContractError('UNSUPPORTED_OPERATION', 'unsupported Factory source class');
|
|
147
|
+
const operation = kind === 'sessions-api' ? 'factory.sessions.read' : 'factory.exec.stream';
|
|
148
|
+
if (source.authorizedScope.networkOperation !== operation
|
|
149
|
+
|| (source.authorizedScope.authorizedAccounts?.length ?? 0) === 0) {
|
|
150
|
+
throw new SessionContractError('OPERATION_NOT_AUTHORIZED', `Factory ${kind} requires explicit account and ${operation} authorization`);
|
|
151
|
+
}
|
|
152
|
+
const transport = this.transports.find((candidate) => candidate.kind === kind);
|
|
153
|
+
if (!transport) {
|
|
154
|
+
throw new SessionContractError('UNSUPPORTED_OPERATION', `Factory ${kind} capability was not negotiated`);
|
|
155
|
+
}
|
|
156
|
+
const values = await transport.snapshot(source);
|
|
157
|
+
if (values.length > (this.limits?.maxRecords ?? 1_000_000)) {
|
|
158
|
+
throw new SessionContractError('RESOURCE_LIMIT_EXCEEDED', 'Factory remote record limit exceeded');
|
|
159
|
+
}
|
|
160
|
+
return values.map((value, sequence) => ({
|
|
161
|
+
value, sequence, byteOffset: sequence, byteLength: Buffer.byteLength(JSON.stringify(value)),
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function normalize(input, source) {
|
|
166
|
+
const output = [];
|
|
167
|
+
const filenameId = basename(source.locator, extname(source.locator));
|
|
168
|
+
let complete = false;
|
|
169
|
+
let establishedId;
|
|
170
|
+
for (const line of input) {
|
|
171
|
+
const parsed = FactoryRecordSchema.safeParse(line.value);
|
|
172
|
+
if (!parsed.success) {
|
|
173
|
+
throw new SessionContractError('MALFORMED_SOURCE', 'Factory session record is malformed');
|
|
174
|
+
}
|
|
175
|
+
const value = parsed.data;
|
|
176
|
+
const nativeSessionId = value.sessionId ?? value.session_id ?? establishedId ?? filenameId;
|
|
177
|
+
if (establishedId && nativeSessionId !== establishedId) {
|
|
178
|
+
throw new SessionContractError('DUPLICATE_NATIVE_ID', 'Factory source changes session identity');
|
|
179
|
+
}
|
|
180
|
+
establishedId = nativeSessionId;
|
|
181
|
+
complete ||= ['session_end', 'result'].includes(value.type)
|
|
182
|
+
|| ['completed', 'archived', 'deleted'].includes(value.status ?? '');
|
|
183
|
+
const blocks = contentBlocks(value);
|
|
184
|
+
if (blocks.length === 0)
|
|
185
|
+
blocks.push({
|
|
186
|
+
kind: value.type === 'settings' ? 'factory.settings' : `factory.${value.type}`,
|
|
187
|
+
text: '',
|
|
188
|
+
opaque: value.type !== 'settings',
|
|
189
|
+
nativeId: undefined,
|
|
190
|
+
unknownFields: {},
|
|
191
|
+
});
|
|
192
|
+
blocks.forEach((block, index) => output.push({
|
|
193
|
+
nativeSessionId,
|
|
194
|
+
nativeEventId: block.nativeId
|
|
195
|
+
?? (value.uuid ?? value.id ?? value.message?.id
|
|
196
|
+
? `${value.uuid ?? value.id ?? value.message?.id}:${index}`
|
|
197
|
+
: undefined),
|
|
198
|
+
sequence: line.sequence * 1_000 + index,
|
|
199
|
+
kind: block.kind,
|
|
200
|
+
role: value.message?.role,
|
|
201
|
+
occurredAt: timestamp(value.timestamp ?? value.updatedAt ?? value.createdAt),
|
|
202
|
+
text: block.text,
|
|
203
|
+
rawReference: { locatorClass: source.locatorClass, offset: line.byteOffset },
|
|
204
|
+
extensions: {
|
|
205
|
+
subtype: value.subtype,
|
|
206
|
+
parentUuid: value.parentUuid,
|
|
207
|
+
productVersion: value.version ?? 'not-reported',
|
|
208
|
+
lifecycle: complete ? lifecycle(value.status) : 'active',
|
|
209
|
+
workspace: { cwdClass: value.cwd ? '<workspace>' : undefined },
|
|
210
|
+
settings: value.sessionSettings ?? value.settings,
|
|
211
|
+
opaque: block.opaque,
|
|
212
|
+
provenance: {
|
|
213
|
+
acquisition: source.locatorClass,
|
|
214
|
+
schema: value.schemaVersion ?? FACTORY_SOURCE_SCHEMA_VERSION,
|
|
215
|
+
apiNegotiated: source.locatorClass === 'factory-sessions-api',
|
|
216
|
+
execNegotiated: source.locatorClass === 'factory-exec-stream',
|
|
217
|
+
},
|
|
218
|
+
unknownFields: {
|
|
219
|
+
...unknownFields(value, RECORD_KEYS),
|
|
220
|
+
...block.unknownFields,
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
225
|
+
return { records: output, complete };
|
|
226
|
+
}
|
|
227
|
+
function contentBlocks(value) {
|
|
228
|
+
const content = value.message?.content ?? value.content;
|
|
229
|
+
if (typeof content === 'string') {
|
|
230
|
+
return [{ kind: 'message', text: content, opaque: false, unknownFields: {} }];
|
|
231
|
+
}
|
|
232
|
+
if (!Array.isArray(content))
|
|
233
|
+
return [];
|
|
234
|
+
return content.map((item) => {
|
|
235
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
236
|
+
return { kind: 'factory.unknown-block', text: '', opaque: true, unknownFields: { value: item } };
|
|
237
|
+
}
|
|
238
|
+
const block = item;
|
|
239
|
+
const type = typeof block.type === 'string' ? block.type : 'unknown';
|
|
240
|
+
const nativeId = typeof block.id === 'string'
|
|
241
|
+
? block.id
|
|
242
|
+
: typeof block.tool_use_id === 'string' ? block.tool_use_id : undefined;
|
|
243
|
+
if (type === 'text') {
|
|
244
|
+
return { kind: 'message', text: stringValue(block.text), opaque: false, nativeId,
|
|
245
|
+
unknownFields: unknownFields(block, new Set(['type', 'id', 'text'])) };
|
|
246
|
+
}
|
|
247
|
+
if (type === 'reasoning' || type === 'thinking') {
|
|
248
|
+
return { kind: 'reasoning', text: stringValue(block.text ?? block.thinking),
|
|
249
|
+
opaque: false, nativeId, unknownFields: unknownFields(block, new Set(['type', 'id', 'text', 'thinking'])) };
|
|
250
|
+
}
|
|
251
|
+
if (type === 'tool_use') {
|
|
252
|
+
return { kind: 'tool-call', text: stringValue(block.name), opaque: false, nativeId,
|
|
253
|
+
unknownFields: unknownFields(block, new Set(['type', 'id', 'name', 'input'])) };
|
|
254
|
+
}
|
|
255
|
+
if (type === 'tool_result') {
|
|
256
|
+
return { kind: 'tool-result', text: extractText(block.content), opaque: false, nativeId,
|
|
257
|
+
unknownFields: unknownFields(block, new Set(['type', 'tool_use_id', 'content', 'is_error'])) };
|
|
258
|
+
}
|
|
259
|
+
if (type === 'image') {
|
|
260
|
+
return { kind: 'image', text: '', opaque: true, nativeId,
|
|
261
|
+
unknownFields: { mediaType: block.media_type ?? block.source } };
|
|
262
|
+
}
|
|
263
|
+
return { kind: `factory.${type}`, text: '', opaque: true, nativeId, unknownFields: { ...block } };
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
async function* discoverJsonl(root, maxDepth) {
|
|
267
|
+
const pending = [{ path: root, depth: 0 }];
|
|
268
|
+
while (pending.length) {
|
|
269
|
+
const current = pending.shift();
|
|
270
|
+
let directory;
|
|
271
|
+
try {
|
|
272
|
+
directory = await opendir(current.path);
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
throw new SessionContractError('SOURCE_NOT_AUTHORIZED', 'authorized Factory root is inaccessible');
|
|
276
|
+
}
|
|
277
|
+
const directories = [];
|
|
278
|
+
const files = [];
|
|
279
|
+
for await (const entry of directory) {
|
|
280
|
+
const path = resolve(current.path, entry.name);
|
|
281
|
+
if (entry.isSymbolicLink())
|
|
282
|
+
continue;
|
|
283
|
+
if (entry.isDirectory() && current.depth < maxDepth)
|
|
284
|
+
directories.push(path);
|
|
285
|
+
else if (entry.isFile() && entry.name.endsWith('.jsonl'))
|
|
286
|
+
files.push(path);
|
|
287
|
+
}
|
|
288
|
+
for (const file of files.sort())
|
|
289
|
+
yield file;
|
|
290
|
+
for (const path of directories.sort())
|
|
291
|
+
pending.push({ path, depth: current.depth + 1 });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function declaredVersion(records) {
|
|
295
|
+
const versions = new Set(records.flatMap((record) => {
|
|
296
|
+
const value = asObject(record.value);
|
|
297
|
+
return typeof value.schemaVersion === 'string' ? [value.schemaVersion] : [];
|
|
298
|
+
}));
|
|
299
|
+
if (versions.size > 1)
|
|
300
|
+
throw new SessionContractError('SCHEMA_DRIFT', 'mixed Factory schema versions');
|
|
301
|
+
return [...versions][0] ?? FACTORY_SOURCE_SCHEMA_VERSION;
|
|
302
|
+
}
|
|
303
|
+
function timestamp(value) {
|
|
304
|
+
if (value === undefined)
|
|
305
|
+
return undefined;
|
|
306
|
+
const date = new Date(value);
|
|
307
|
+
return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
|
|
308
|
+
}
|
|
309
|
+
function lifecycle(status) {
|
|
310
|
+
if (status === 'archived')
|
|
311
|
+
return 'archived';
|
|
312
|
+
if (status === 'deleted')
|
|
313
|
+
return 'deleted';
|
|
314
|
+
return 'complete';
|
|
315
|
+
}
|
|
316
|
+
function extractText(value) {
|
|
317
|
+
if (typeof value === 'string')
|
|
318
|
+
return value;
|
|
319
|
+
if (!Array.isArray(value))
|
|
320
|
+
return '';
|
|
321
|
+
return value.map((item) => stringValue(asObject(item).text)).filter(Boolean).join('\n');
|
|
322
|
+
}
|
|
323
|
+
function stringValue(value) {
|
|
324
|
+
return typeof value === 'string' ? value : '';
|
|
325
|
+
}
|
|
326
|
+
function asObject(value) {
|
|
327
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
328
|
+
? value : {};
|
|
329
|
+
}
|
|
330
|
+
function unknownFields(value, keys) {
|
|
331
|
+
return Object.fromEntries(Object.entries(value).filter(([key]) => !keys.has(key)).sort(([a], [b]) => a.localeCompare(b)));
|
|
332
|
+
}
|
|
333
|
+
function parseCursor(value) {
|
|
334
|
+
if (!value)
|
|
335
|
+
return 0;
|
|
336
|
+
if (!/^\d+$/.test(value))
|
|
337
|
+
throw new SessionContractError('SCHEMA_DRIFT', 'invalid Factory cursor');
|
|
338
|
+
return Number(value);
|
|
339
|
+
}
|
|
340
|
+
const RECORD_KEYS = new Set([
|
|
341
|
+
'schemaVersion', 'type', 'subtype', 'uuid', 'id', 'parentUuid', 'sessionId',
|
|
342
|
+
'session_id', 'timestamp', 'createdAt', 'updatedAt', 'cwd', 'version', 'message',
|
|
343
|
+
'content', 'sessionSettings', 'settings', 'status',
|
|
344
|
+
]);
|
|
345
|
+
//# sourceMappingURL=factory.js.map
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { SessionContractError, assertSupportedSchemaMajor, } from '../contracts.js';
|
|
3
|
+
import { streamBoundedJsonLines } from '../readers.js';
|
|
4
|
+
export const GENERIC_INTERCHANGE_KIND = 'aiwg.session-interchange';
|
|
5
|
+
export const GENERIC_ADAPTER_VERSION = '1.0.0';
|
|
6
|
+
const Rfc3339Schema = z.string().refine((value) => /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value)
|
|
7
|
+
&& !Number.isNaN(Date.parse(value)), 'timestamp must be RFC 3339 with an explicit UTC offset');
|
|
8
|
+
export const GenericInterchangeHeaderSchema = z.object({
|
|
9
|
+
type: z.literal(GENERIC_INTERCHANGE_KIND),
|
|
10
|
+
schemaVersion: z.string().regex(/^\d+\.\d+\.\d+$/),
|
|
11
|
+
product: z.string().min(1),
|
|
12
|
+
productVersion: z.string().min(1),
|
|
13
|
+
sourceId: z.string().min(1),
|
|
14
|
+
exportedAt: Rfc3339Schema,
|
|
15
|
+
consistency: z.enum(['provisional', 'consistent-snapshot', 'complete']),
|
|
16
|
+
lifecycle: z.enum(['active', 'complete', 'archived']),
|
|
17
|
+
workspace: z.object({
|
|
18
|
+
id: z.string().min(1),
|
|
19
|
+
name: z.string().min(1).optional(),
|
|
20
|
+
repository: z.string().min(1).optional(),
|
|
21
|
+
branch: z.string().min(1).optional(),
|
|
22
|
+
}).passthrough(),
|
|
23
|
+
provenance: z.object({
|
|
24
|
+
exporter: z.string().min(1),
|
|
25
|
+
exporterVersion: z.string().min(1),
|
|
26
|
+
sourceClass: z.string().min(1),
|
|
27
|
+
}).passthrough(),
|
|
28
|
+
extensions: z.record(z.unknown()).default({}),
|
|
29
|
+
}).passthrough();
|
|
30
|
+
export const GenericInterchangeEventSchema = z.object({
|
|
31
|
+
type: z.literal('event'),
|
|
32
|
+
sessionId: z.string().min(1),
|
|
33
|
+
eventId: z.string().min(1),
|
|
34
|
+
sequence: z.number().int().nonnegative(),
|
|
35
|
+
kind: z.string().min(1),
|
|
36
|
+
role: z.string().min(1).optional(),
|
|
37
|
+
participant: z.string().min(1).optional(),
|
|
38
|
+
toolName: z.string().min(1).optional(),
|
|
39
|
+
toolCallId: z.string().min(1).optional(),
|
|
40
|
+
model: z.string().min(1).optional(),
|
|
41
|
+
entities: z.array(z.string().min(1)).optional(),
|
|
42
|
+
extractionState: z.string().min(1).optional(),
|
|
43
|
+
occurredAt: Rfc3339Schema.optional(),
|
|
44
|
+
text: z.string(),
|
|
45
|
+
lifecycle: z.enum(['active', 'complete', 'archived']),
|
|
46
|
+
extensions: z.record(z.unknown()).default({}),
|
|
47
|
+
}).passthrough();
|
|
48
|
+
export class GenericSessionInterchangeAdapter {
|
|
49
|
+
limits;
|
|
50
|
+
provider = 'generic';
|
|
51
|
+
adapterVersion = GENERIC_ADAPTER_VERSION;
|
|
52
|
+
disposition = 'manual-only';
|
|
53
|
+
supportedOperations = ['inspect', 'stream'];
|
|
54
|
+
acquisitionModes = ['manual-export'];
|
|
55
|
+
constructor(limits) {
|
|
56
|
+
this.limits = limits;
|
|
57
|
+
}
|
|
58
|
+
async *discover(_scope) {
|
|
59
|
+
throw new SessionContractError('UNSUPPORTED_OPERATION', 'generic interchange does not support automatic discovery; select an exported file explicitly');
|
|
60
|
+
}
|
|
61
|
+
async inspect(source) {
|
|
62
|
+
const input = await this.openStream(source);
|
|
63
|
+
let header = null;
|
|
64
|
+
const eventIds = new Set();
|
|
65
|
+
const sessionSequences = new Set();
|
|
66
|
+
for await (const record of input) {
|
|
67
|
+
if (!header) {
|
|
68
|
+
header = this.validateHeader(record.value, source);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const event = parseEvent(record.value);
|
|
72
|
+
if (eventIds.has(event.eventId)) {
|
|
73
|
+
throw new SessionContractError('DUPLICATE_NATIVE_ID', 'generic interchange contains a duplicate event ID');
|
|
74
|
+
}
|
|
75
|
+
eventIds.add(event.eventId);
|
|
76
|
+
const sequenceKey = `${event.sessionId}\0${event.sequence}`;
|
|
77
|
+
if (sessionSequences.has(sequenceKey)) {
|
|
78
|
+
throw new SessionContractError('DUPLICATE_NATIVE_ID', 'generic interchange contains a duplicate session sequence');
|
|
79
|
+
}
|
|
80
|
+
sessionSequences.add(sequenceKey);
|
|
81
|
+
}
|
|
82
|
+
if (!header)
|
|
83
|
+
throw new SessionContractError('MALFORMED_SOURCE', 'generic interchange is empty');
|
|
84
|
+
if (input.incompleteTail) {
|
|
85
|
+
throw new SessionContractError('TRUNCATED_SOURCE', 'generic interchange contains a truncated record');
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
sourceSchemaVersion: header.schemaVersion,
|
|
89
|
+
consistency: header.consistency,
|
|
90
|
+
operationalState: 'available',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async *stream(source, cursor) {
|
|
94
|
+
const byteCursor = parseByteCursor(cursor?.value);
|
|
95
|
+
let header = null;
|
|
96
|
+
if (byteCursor !== null) {
|
|
97
|
+
const headerStream = await this.openStream(source);
|
|
98
|
+
const iterator = headerStream[Symbol.asyncIterator]();
|
|
99
|
+
const first = await iterator.next();
|
|
100
|
+
await iterator.return?.();
|
|
101
|
+
if (first.done)
|
|
102
|
+
throw new SessionContractError('MALFORMED_SOURCE', 'generic interchange is empty');
|
|
103
|
+
header = this.validateHeader(first.value.value, source);
|
|
104
|
+
}
|
|
105
|
+
const input = await this.openStream(source, byteCursor ?? undefined);
|
|
106
|
+
const start = byteCursor === null ? parseEventCursor(cursor?.value) : 0;
|
|
107
|
+
let eventIndex = 0;
|
|
108
|
+
const eventIds = new Set();
|
|
109
|
+
const sessionSequences = new Set();
|
|
110
|
+
for await (const record of input) {
|
|
111
|
+
if (!header) {
|
|
112
|
+
header = this.validateHeader(record.value, source);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const event = parseEvent(record.value);
|
|
116
|
+
if (eventIds.has(event.eventId)) {
|
|
117
|
+
throw new SessionContractError('DUPLICATE_NATIVE_ID', 'generic interchange contains a duplicate event ID');
|
|
118
|
+
}
|
|
119
|
+
eventIds.add(event.eventId);
|
|
120
|
+
const sequenceKey = `${event.sessionId}\0${event.sequence}`;
|
|
121
|
+
if (sessionSequences.has(sequenceKey)) {
|
|
122
|
+
throw new SessionContractError('DUPLICATE_NATIVE_ID', 'generic interchange contains a duplicate session sequence');
|
|
123
|
+
}
|
|
124
|
+
sessionSequences.add(sequenceKey);
|
|
125
|
+
if (eventIndex++ < start)
|
|
126
|
+
continue;
|
|
127
|
+
const knownKeys = new Set([
|
|
128
|
+
'type', 'sessionId', 'eventId', 'sequence', 'kind', 'role',
|
|
129
|
+
'occurredAt', 'text', 'lifecycle', 'extensions', 'participant',
|
|
130
|
+
'toolName', 'toolCallId', 'model', 'entities', 'extractionState',
|
|
131
|
+
]);
|
|
132
|
+
const unknownFields = Object.fromEntries(Object.entries(event).filter(([key]) => !knownKeys.has(key)));
|
|
133
|
+
yield {
|
|
134
|
+
nativeSessionId: event.sessionId,
|
|
135
|
+
nativeEventId: event.eventId,
|
|
136
|
+
sequence: event.sequence,
|
|
137
|
+
kind: event.kind,
|
|
138
|
+
role: event.role,
|
|
139
|
+
participant: event.participant,
|
|
140
|
+
toolName: event.toolName,
|
|
141
|
+
toolCallId: event.toolCallId,
|
|
142
|
+
model: event.model,
|
|
143
|
+
entities: event.entities,
|
|
144
|
+
extractionState: event.extractionState,
|
|
145
|
+
sourceCursor: `byte:${record.byteOffset + record.byteLength}`,
|
|
146
|
+
sourceBytes: record.byteLength,
|
|
147
|
+
occurredAt: event.occurredAt,
|
|
148
|
+
text: event.text,
|
|
149
|
+
rawReference: { locatorClass: 'generic-interchange', sequence: event.sequence },
|
|
150
|
+
extensions: {
|
|
151
|
+
...event.extensions,
|
|
152
|
+
unknownFields,
|
|
153
|
+
lifecycle: event.lifecycle,
|
|
154
|
+
workspace: header.workspace,
|
|
155
|
+
provenance: header.provenance,
|
|
156
|
+
product: header.product,
|
|
157
|
+
productVersion: header.productVersion,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (!header) {
|
|
162
|
+
throw new SessionContractError('MALFORMED_SOURCE', 'generic interchange is empty');
|
|
163
|
+
}
|
|
164
|
+
if (input.incompleteTail) {
|
|
165
|
+
throw new SessionContractError('TRUNCATED_SOURCE', 'generic interchange contains a truncated record');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
openStream(source, cursor) {
|
|
169
|
+
return streamBoundedJsonLines({
|
|
170
|
+
selectedPath: source.locator,
|
|
171
|
+
allowedRoots: source.authorizedScope.allowedRoots,
|
|
172
|
+
}, {
|
|
173
|
+
cursor: cursor === undefined ? undefined : String(cursor),
|
|
174
|
+
consistency: 'provisional',
|
|
175
|
+
limits: this.limits,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
validateHeader(value, source) {
|
|
179
|
+
const header = parseHeader(value);
|
|
180
|
+
assertSupportedSchemaMajor(header.schemaVersion);
|
|
181
|
+
if (header.sourceId !== source.sourceId) {
|
|
182
|
+
throw new SessionContractError('SCHEMA_DRIFT', 'selected source identity does not match interchange header');
|
|
183
|
+
}
|
|
184
|
+
return header;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function parseByteCursor(value) {
|
|
188
|
+
if (!value?.startsWith('byte:'))
|
|
189
|
+
return null;
|
|
190
|
+
const position = value.slice(5);
|
|
191
|
+
if (!/^\d+$/.test(position)) {
|
|
192
|
+
throw new SessionContractError('SCHEMA_DRIFT', 'invalid generic byte cursor');
|
|
193
|
+
}
|
|
194
|
+
return Number(position);
|
|
195
|
+
}
|
|
196
|
+
function parseHeader(value) {
|
|
197
|
+
const result = GenericInterchangeHeaderSchema.safeParse(value);
|
|
198
|
+
if (result.success)
|
|
199
|
+
return result.data;
|
|
200
|
+
if (hasAmbiguousTimestamp(result.error)) {
|
|
201
|
+
throw new SessionContractError('AMBIGUOUS_TIMESTAMP', 'generic interchange header has an ambiguous timestamp');
|
|
202
|
+
}
|
|
203
|
+
throw new SessionContractError('MALFORMED_SOURCE', 'input is not a declared AIWG generic session interchange');
|
|
204
|
+
}
|
|
205
|
+
function parseEvent(value) {
|
|
206
|
+
const result = GenericInterchangeEventSchema.safeParse(value);
|
|
207
|
+
if (result.success)
|
|
208
|
+
return result.data;
|
|
209
|
+
if (hasAmbiguousTimestamp(result.error)) {
|
|
210
|
+
throw new SessionContractError('AMBIGUOUS_TIMESTAMP', 'generic interchange event has an ambiguous timestamp');
|
|
211
|
+
}
|
|
212
|
+
throw new SessionContractError('MALFORMED_SOURCE', 'generic interchange event is malformed');
|
|
213
|
+
}
|
|
214
|
+
function hasAmbiguousTimestamp(error) {
|
|
215
|
+
return error.issues.some((issue) => issue.code === z.ZodIssueCode.custom
|
|
216
|
+
&& (issue.path.at(-1) === 'occurredAt' || issue.path.at(-1) === 'exportedAt'));
|
|
217
|
+
}
|
|
218
|
+
function parseEventCursor(value) {
|
|
219
|
+
if (value === undefined || value === '')
|
|
220
|
+
return 0;
|
|
221
|
+
if (!/^\d+$/.test(value))
|
|
222
|
+
throw new SessionContractError('SCHEMA_DRIFT', 'generic event cursor is invalid');
|
|
223
|
+
return Number(value);
|
|
224
|
+
}
|
|
225
|
+
//# sourceMappingURL=generic.js.map
|