@nifrajs/coding-agent 3.3.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -4
- package/SECURITY.md +2 -1
- package/dist/approvals.d.ts +14 -2
- package/dist/approvals.d.ts.map +1 -1
- package/dist/approvals.js +51 -43
- package/dist/approvals.js.map +1 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +11 -2
- package/dist/cli.js.map +1 -1
- package/dist/context.js +29 -1
- package/dist/context.js.map +1 -1
- package/dist/diff.js +38 -2
- package/dist/diff.js.map +1 -1
- package/dist/handoffs.d.ts.map +1 -1
- package/dist/handoffs.js +12 -4
- package/dist/handoffs.js.map +1 -1
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +146 -23
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/isolated.js +37 -2
- package/dist/isolated.js.map +1 -1
- package/dist/native.d.ts +12 -0
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +165 -40
- package/dist/native.js.map +1 -1
- package/dist/orchestration/compile.d.ts +7 -0
- package/dist/orchestration/compile.d.ts.map +1 -1
- package/dist/orchestration/compile.js +16 -2
- package/dist/orchestration/compile.js.map +1 -1
- package/dist/orchestration/evidence-store.d.ts.map +1 -1
- package/dist/orchestration/evidence-store.js +9 -4
- package/dist/orchestration/evidence-store.js.map +1 -1
- package/dist/orchestration/host.d.ts +10 -0
- package/dist/orchestration/host.d.ts.map +1 -1
- package/dist/orchestration/host.js +9 -0
- package/dist/orchestration/host.js.map +1 -1
- package/dist/replay.d.ts +4 -0
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +189 -24
- package/dist/replay.js.map +1 -1
- package/dist/rpc.d.ts +10 -0
- package/dist/rpc.d.ts.map +1 -1
- package/dist/rpc.js +209 -29
- package/dist/rpc.js.map +1 -1
- package/dist/session-migration.d.ts.map +1 -1
- package/dist/session-migration.js +3 -2
- package/dist/session-migration.js.map +1 -1
- package/dist/sessions.d.ts +20 -2
- package/dist/sessions.d.ts.map +1 -1
- package/dist/sessions.js +269 -60
- package/dist/sessions.js.map +1 -1
- package/dist/subagents.d.ts +30 -0
- package/dist/subagents.d.ts.map +1 -1
- package/dist/subagents.js +118 -15
- package/dist/subagents.js.map +1 -1
- package/dist/verification.d.ts +28 -1
- package/dist/verification.d.ts.map +1 -1
- package/dist/verification.js +119 -3
- package/dist/verification.js.map +1 -1
- package/dist/workflows.d.ts +1 -0
- package/dist/workflows.d.ts.map +1 -1
- package/dist/workflows.js +55 -23
- package/dist/workflows.js.map +1 -1
- package/package.json +9 -5
- package/src/approvals.ts +62 -45
- package/src/cli.ts +11 -2
- package/src/context.ts +32 -1
- package/src/diff.ts +40 -2
- package/src/handoffs.ts +20 -8
- package/src/host.ts +146 -23
- package/src/index.ts +7 -0
- package/src/isolated.ts +39 -2
- package/src/native.ts +213 -39
- package/src/orchestration/compile.ts +28 -2
- package/src/orchestration/evidence-store.ts +9 -3
- package/src/orchestration/host.ts +17 -0
- package/src/replay.ts +206 -25
- package/src/rpc.ts +262 -33
- package/src/session-migration.ts +3 -2
- package/src/sessions.ts +288 -57
- package/src/subagents.ts +147 -17
- package/src/verification.ts +146 -3
- package/src/workflows.ts +47 -27
package/src/host.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
agentError,
|
|
9
9
|
type CreateSessionInput,
|
|
10
10
|
type ForkSessionResult,
|
|
11
|
+
isAgentEvent,
|
|
11
12
|
type ReloadResult,
|
|
12
13
|
} from "@nifrajs/agent-protocol"
|
|
13
14
|
import {
|
|
@@ -86,18 +87,33 @@ export class CodingAgentHost {
|
|
|
86
87
|
|
|
87
88
|
async start(input: CreateSessionInput): Promise<AgentSessionSnapshot> {
|
|
88
89
|
if (this.session !== undefined) throw new Error("coding agent: a session is already active")
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
history
|
|
93
|
-
this.
|
|
94
|
-
|
|
90
|
+
const created = await this.backend.createSession(input)
|
|
91
|
+
this.session = created
|
|
92
|
+
try {
|
|
93
|
+
let history: readonly import("./sessions.ts").SessionLogEntry[] = []
|
|
94
|
+
if (input.sessionId !== undefined && this.sessionStore !== undefined) {
|
|
95
|
+
history = await this.sessionStore.read(input.sessionId)
|
|
96
|
+
this.context.restore(history.map(sessionEntryToContextRecord))
|
|
97
|
+
} else {
|
|
98
|
+
this.context.restore([])
|
|
99
|
+
}
|
|
100
|
+
this.nextSeq = nextSequence(created, history)
|
|
101
|
+
await this.extensions?.reload()
|
|
102
|
+
await this.sessionStore?.append(created.id, "session.started", created)
|
|
103
|
+
return created
|
|
104
|
+
} catch (error) {
|
|
105
|
+
// Session creation may allocate a process or other external resource. If history, extension
|
|
106
|
+
// initialization, or the first persistence write fails, roll the backend session back before
|
|
107
|
+
// exposing the failure; otherwise a retry leaks the live session behind `this.session = undefined`.
|
|
108
|
+
this.session = undefined
|
|
95
109
|
this.context.restore([])
|
|
110
|
+
this.nextSeq = 0
|
|
111
|
+
await this.backend.cancel(created.id, "session startup failed").catch(() => {})
|
|
112
|
+
await this.backend.close(created.id).catch(() => {})
|
|
113
|
+
await this.extensions?.close().catch(() => {})
|
|
114
|
+
this.approvals.close()
|
|
115
|
+
throw error
|
|
96
116
|
}
|
|
97
|
-
this.nextSeq = nextSequence(this.session, history)
|
|
98
|
-
await this.extensions?.reload()
|
|
99
|
-
await this.sessionStore?.append(this.session.id, "session.started", this.session)
|
|
100
|
-
return this.session
|
|
101
117
|
}
|
|
102
118
|
|
|
103
119
|
get snapshot(): AgentSessionSnapshot | undefined {
|
|
@@ -119,6 +135,12 @@ export class CodingAgentHost {
|
|
|
119
135
|
const event = this.acceptBackendEvent(rawEvent)
|
|
120
136
|
await this.recordEvent(event)
|
|
121
137
|
yield event
|
|
138
|
+
if (event.type === "session.stopped") return
|
|
139
|
+
if (event.type === "session.failed") {
|
|
140
|
+
if (event.error.code === "BACKEND_PROTOCOL")
|
|
141
|
+
await this.backend.cancel(sessionId, "backend protocol error").catch(() => {})
|
|
142
|
+
return
|
|
143
|
+
}
|
|
122
144
|
}
|
|
123
145
|
if (signal?.aborted === true) return
|
|
124
146
|
|
|
@@ -133,8 +155,15 @@ export class CodingAgentHost {
|
|
|
133
155
|
|
|
134
156
|
private nextSeq = 0
|
|
135
157
|
|
|
136
|
-
private acceptBackendEvent(rawEvent:
|
|
158
|
+
private acceptBackendEvent(rawEvent: unknown): AgentEvent {
|
|
137
159
|
const session = this.requireSession()
|
|
160
|
+
if (!isAgentEvent(rawEvent) || rawEvent.sessionId !== session.id) {
|
|
161
|
+
return this.createEvent({
|
|
162
|
+
type: "session.failed",
|
|
163
|
+
error: agentError("BACKEND_PROTOCOL", "backend emitted an invalid agent event"),
|
|
164
|
+
recoverable: false,
|
|
165
|
+
})
|
|
166
|
+
}
|
|
138
167
|
const event = Object.freeze({
|
|
139
168
|
...rawEvent,
|
|
140
169
|
sessionId: session.id,
|
|
@@ -153,6 +182,26 @@ export class CodingAgentHost {
|
|
|
153
182
|
this.session = snapshot
|
|
154
183
|
return Object.freeze({ ...event, snapshot }) as AgentEvent
|
|
155
184
|
}
|
|
185
|
+
if (event.type === "session.stopped") {
|
|
186
|
+
const { activeTurnId: _activeTurnId, ...withoutActiveTurn } = session
|
|
187
|
+
this.session = Object.freeze({
|
|
188
|
+
...withoutActiveTurn,
|
|
189
|
+
status: "stopped",
|
|
190
|
+
lastSeq: event.seq,
|
|
191
|
+
updatedAt: event.at,
|
|
192
|
+
})
|
|
193
|
+
return event
|
|
194
|
+
}
|
|
195
|
+
if (event.type === "session.failed") {
|
|
196
|
+
const { activeTurnId: _activeTurnId, ...withoutActiveTurn } = session
|
|
197
|
+
this.session = Object.freeze({
|
|
198
|
+
...withoutActiveTurn,
|
|
199
|
+
status: "failed",
|
|
200
|
+
lastSeq: event.seq,
|
|
201
|
+
updatedAt: event.at,
|
|
202
|
+
})
|
|
203
|
+
return event
|
|
204
|
+
}
|
|
156
205
|
this.session = Object.freeze({
|
|
157
206
|
...session,
|
|
158
207
|
lastSeq: event.seq,
|
|
@@ -200,18 +249,30 @@ export class CodingAgentHost {
|
|
|
200
249
|
|
|
201
250
|
private createEvent(payload: AgentEventPayload): AgentEvent {
|
|
202
251
|
const session = this.requireSession()
|
|
203
|
-
const
|
|
252
|
+
const candidate = Object.freeze({
|
|
204
253
|
version: 1 as const,
|
|
205
254
|
sessionId: session.id,
|
|
206
255
|
seq: this.nextSeq++,
|
|
207
256
|
at: Date.now(),
|
|
208
257
|
...payload,
|
|
209
258
|
}) as AgentEvent
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
259
|
+
const event = normalizeHostEvent(candidate)
|
|
260
|
+
if (event === undefined) throw new Error("coding agent: host generated an invalid agent event")
|
|
261
|
+
if (event.type === "session.stopped" || event.type === "session.failed") {
|
|
262
|
+
const { activeTurnId: _activeTurnId, ...withoutActiveTurn } = session
|
|
263
|
+
this.session = Object.freeze({
|
|
264
|
+
...withoutActiveTurn,
|
|
265
|
+
status: event.type === "session.stopped" ? "stopped" : "failed",
|
|
266
|
+
lastSeq: event.seq,
|
|
267
|
+
updatedAt: event.at,
|
|
268
|
+
})
|
|
269
|
+
} else {
|
|
270
|
+
this.session = Object.freeze({
|
|
271
|
+
...session,
|
|
272
|
+
lastSeq: event.seq,
|
|
273
|
+
updatedAt: event.at,
|
|
274
|
+
})
|
|
275
|
+
}
|
|
215
276
|
return event
|
|
216
277
|
}
|
|
217
278
|
|
|
@@ -310,7 +371,7 @@ export class CodingAgentHost {
|
|
|
310
371
|
if (this.sessionStore === undefined) return Object.freeze([])
|
|
311
372
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 4096)
|
|
312
373
|
throw new RangeError("coding agent: history limit must be between 1 and 4096")
|
|
313
|
-
const entries = await this.sessionStore.read(this.session.id)
|
|
374
|
+
const entries = await this.sessionStore.read(this.session.id, limit)
|
|
314
375
|
return Object.freeze(entries.slice(-limit))
|
|
315
376
|
}
|
|
316
377
|
|
|
@@ -318,11 +379,16 @@ export class CodingAgentHost {
|
|
|
318
379
|
if (this.session === undefined) return
|
|
319
380
|
const id = this.session.id
|
|
320
381
|
this.session = undefined
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
382
|
+
try {
|
|
383
|
+
await this.sessionStore?.append(id, "session.stopped", { reason })
|
|
384
|
+
} finally {
|
|
385
|
+
// Persistence is an audit aid, not a prerequisite for releasing the backend. Always clean up
|
|
386
|
+
// the live session even when the store is full, read-only, or temporarily unavailable.
|
|
387
|
+
await this.backend.cancel(id, reason).catch(() => {})
|
|
388
|
+
await this.backend.close(id).catch(() => {})
|
|
389
|
+
await this.extensions?.close().catch(() => {})
|
|
390
|
+
this.approvals.close()
|
|
391
|
+
}
|
|
326
392
|
}
|
|
327
393
|
|
|
328
394
|
get pendingApprovals(): readonly ApprovalRequest[] {
|
|
@@ -394,6 +460,63 @@ function eventText(event: AgentEvent): string {
|
|
|
394
460
|
}
|
|
395
461
|
}
|
|
396
462
|
|
|
463
|
+
/** Keep host-owned diagnostics inside the same wire contract as backend events. */
|
|
464
|
+
function normalizeHostEvent(event: unknown): AgentEvent | undefined {
|
|
465
|
+
if (isAgentEvent(event)) return event
|
|
466
|
+
if (event === null || typeof event !== "object" || Array.isArray(event)) return undefined
|
|
467
|
+
const candidate = event as Record<string, unknown>
|
|
468
|
+
if (candidate.type === "verification.completed") {
|
|
469
|
+
// Verification reports are command output. They are useful to the repair prompt, but an
|
|
470
|
+
// arbitrary report must not bypass the protocol's depth/object/string limits on the live stream.
|
|
471
|
+
const { report: _report, ...withoutReport } = candidate
|
|
472
|
+
const fallback = Object.freeze(withoutReport)
|
|
473
|
+
return isAgentEvent(fallback) ? (fallback as AgentEvent) : undefined
|
|
474
|
+
}
|
|
475
|
+
if (candidate.type !== "repair.required") return undefined
|
|
476
|
+
|
|
477
|
+
const source = candidate.task
|
|
478
|
+
if (source === null || typeof source !== "object" || Array.isArray(source)) return undefined
|
|
479
|
+
const taskSource = source as Record<string, unknown>
|
|
480
|
+
const task = Object.freeze({
|
|
481
|
+
id: boundedHostToken(taskSource.id)
|
|
482
|
+
? taskSource.id
|
|
483
|
+
: `verification-${typeof candidate.seq === "number" ? candidate.seq : "unknown"}`,
|
|
484
|
+
verification:
|
|
485
|
+
taskSource.verification === "check" ||
|
|
486
|
+
taskSource.verification === "assure" ||
|
|
487
|
+
taskSource.verification === "test"
|
|
488
|
+
? taskSource.verification
|
|
489
|
+
: "check",
|
|
490
|
+
cwd: boundedHostText(taskSource.cwd, 4_096) || ".",
|
|
491
|
+
reason: boundedHostText(taskSource.reason, 256 * 1024) || "verification failed",
|
|
492
|
+
capabilities: Object.freeze(
|
|
493
|
+
(Array.isArray(taskSource.capabilities) ? taskSource.capabilities : []).filter(
|
|
494
|
+
boundedHostToken,
|
|
495
|
+
),
|
|
496
|
+
),
|
|
497
|
+
...(typeof taskSource.output === "string"
|
|
498
|
+
? { output: boundedHostText(taskSource.output, 256 * 1024) }
|
|
499
|
+
: {}),
|
|
500
|
+
})
|
|
501
|
+
const { task: _task, turnId, ...base } = candidate
|
|
502
|
+
const fallback = Object.freeze({
|
|
503
|
+
...base,
|
|
504
|
+
...(boundedHostToken(turnId) ? { turnId } : {}),
|
|
505
|
+
task,
|
|
506
|
+
})
|
|
507
|
+
return isAgentEvent(fallback) ? (fallback as AgentEvent) : undefined
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function boundedHostText(value: unknown, max: number): string {
|
|
511
|
+
if (typeof value !== "string") return ""
|
|
512
|
+
const clean = value.replaceAll("\0", "")
|
|
513
|
+
return clean.length <= max ? clean : clean.slice(0, max)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function boundedHostToken(value: unknown): value is string {
|
|
517
|
+
return typeof value === "string" && /^[A-Za-z0-9._:/-]{1,128}$/.test(value)
|
|
518
|
+
}
|
|
519
|
+
|
|
397
520
|
/** Default session evidence is content-free; live protocol consumers still receive the full event. */
|
|
398
521
|
function eventEvidence(event: AgentEvent): unknown {
|
|
399
522
|
switch (event.type) {
|
package/src/index.ts
CHANGED
|
@@ -133,6 +133,8 @@ export {
|
|
|
133
133
|
} from "./sessions.ts"
|
|
134
134
|
export {
|
|
135
135
|
BoundedSubagentRunner,
|
|
136
|
+
type SubagentAbandonment,
|
|
137
|
+
type SubagentAbandonmentLedger,
|
|
136
138
|
type SubagentExecutor,
|
|
137
139
|
type SubagentResult,
|
|
138
140
|
type SubagentRunnerOptions,
|
|
@@ -150,6 +152,11 @@ export {
|
|
|
150
152
|
} from "./ui.ts"
|
|
151
153
|
export {
|
|
152
154
|
createVerificationRepairTask,
|
|
155
|
+
isSafeReviewDiff,
|
|
156
|
+
type ReviewExecutionErrorCode,
|
|
157
|
+
type ReviewExecutionResult,
|
|
158
|
+
type ReviewOptions,
|
|
159
|
+
runNifraReview,
|
|
153
160
|
runNifraVerification,
|
|
154
161
|
type VerificationOptions,
|
|
155
162
|
type VerificationRepairTask,
|
package/src/isolated.ts
CHANGED
|
@@ -336,9 +336,46 @@ function workerError(message: WorkerMessage): Error {
|
|
|
336
336
|
|
|
337
337
|
function filteredEnv(): Record<string, string> {
|
|
338
338
|
const result: Record<string, string> = {}
|
|
339
|
-
|
|
340
|
-
|
|
339
|
+
const names = [
|
|
340
|
+
"PATH",
|
|
341
|
+
"HOME",
|
|
342
|
+
"LANG",
|
|
343
|
+
"LC_ALL",
|
|
344
|
+
"TERM",
|
|
345
|
+
...(process.platform === "win32"
|
|
346
|
+
? [
|
|
347
|
+
"SystemRoot",
|
|
348
|
+
"WINDIR",
|
|
349
|
+
"TEMP",
|
|
350
|
+
"TMP",
|
|
351
|
+
"USERPROFILE",
|
|
352
|
+
"LOCALAPPDATA",
|
|
353
|
+
"APPDATA",
|
|
354
|
+
"COMSPEC",
|
|
355
|
+
"PATHEXT",
|
|
356
|
+
"BUN_INSTALL",
|
|
357
|
+
"HOMEDRIVE",
|
|
358
|
+
"HOMEPATH",
|
|
359
|
+
"USERNAME",
|
|
360
|
+
"USERDOMAIN",
|
|
361
|
+
"ProgramData",
|
|
362
|
+
"ProgramFiles",
|
|
363
|
+
"ProgramFiles(x86)",
|
|
364
|
+
]
|
|
365
|
+
: []),
|
|
366
|
+
]
|
|
367
|
+
for (const name of names) {
|
|
368
|
+
const value = readEnv(name)
|
|
341
369
|
if (value !== undefined) result[name] = value
|
|
342
370
|
}
|
|
343
371
|
return result
|
|
344
372
|
}
|
|
373
|
+
|
|
374
|
+
function readEnv(name: string): string | undefined {
|
|
375
|
+
const exact = process.env[name]
|
|
376
|
+
if (exact !== undefined || process.platform !== "win32") return exact
|
|
377
|
+
const key = Object.keys(process.env).find(
|
|
378
|
+
(candidate) => candidate.toLowerCase() === name.toLowerCase(),
|
|
379
|
+
)
|
|
380
|
+
return key === undefined ? undefined : process.env[key]
|
|
381
|
+
}
|