@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.
Files changed (88) hide show
  1. package/README.md +11 -4
  2. package/SECURITY.md +2 -1
  3. package/dist/approvals.d.ts +14 -2
  4. package/dist/approvals.d.ts.map +1 -1
  5. package/dist/approvals.js +51 -43
  6. package/dist/approvals.js.map +1 -1
  7. package/dist/cli.d.ts +1 -0
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +11 -2
  10. package/dist/cli.js.map +1 -1
  11. package/dist/context.js +29 -1
  12. package/dist/context.js.map +1 -1
  13. package/dist/diff.js +38 -2
  14. package/dist/diff.js.map +1 -1
  15. package/dist/handoffs.d.ts.map +1 -1
  16. package/dist/handoffs.js +12 -4
  17. package/dist/handoffs.js.map +1 -1
  18. package/dist/host.d.ts.map +1 -1
  19. package/dist/host.js +146 -23
  20. package/dist/host.js.map +1 -1
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/isolated.js +37 -2
  26. package/dist/isolated.js.map +1 -1
  27. package/dist/native.d.ts +12 -0
  28. package/dist/native.d.ts.map +1 -1
  29. package/dist/native.js +165 -40
  30. package/dist/native.js.map +1 -1
  31. package/dist/orchestration/compile.d.ts +7 -0
  32. package/dist/orchestration/compile.d.ts.map +1 -1
  33. package/dist/orchestration/compile.js +16 -2
  34. package/dist/orchestration/compile.js.map +1 -1
  35. package/dist/orchestration/evidence-store.d.ts.map +1 -1
  36. package/dist/orchestration/evidence-store.js +9 -4
  37. package/dist/orchestration/evidence-store.js.map +1 -1
  38. package/dist/orchestration/host.d.ts +10 -0
  39. package/dist/orchestration/host.d.ts.map +1 -1
  40. package/dist/orchestration/host.js +9 -0
  41. package/dist/orchestration/host.js.map +1 -1
  42. package/dist/replay.d.ts +4 -0
  43. package/dist/replay.d.ts.map +1 -1
  44. package/dist/replay.js +189 -24
  45. package/dist/replay.js.map +1 -1
  46. package/dist/rpc.d.ts +10 -0
  47. package/dist/rpc.d.ts.map +1 -1
  48. package/dist/rpc.js +209 -29
  49. package/dist/rpc.js.map +1 -1
  50. package/dist/session-migration.d.ts.map +1 -1
  51. package/dist/session-migration.js +3 -2
  52. package/dist/session-migration.js.map +1 -1
  53. package/dist/sessions.d.ts +20 -2
  54. package/dist/sessions.d.ts.map +1 -1
  55. package/dist/sessions.js +269 -60
  56. package/dist/sessions.js.map +1 -1
  57. package/dist/subagents.d.ts +30 -0
  58. package/dist/subagents.d.ts.map +1 -1
  59. package/dist/subagents.js +118 -15
  60. package/dist/subagents.js.map +1 -1
  61. package/dist/verification.d.ts +28 -1
  62. package/dist/verification.d.ts.map +1 -1
  63. package/dist/verification.js +119 -3
  64. package/dist/verification.js.map +1 -1
  65. package/dist/workflows.d.ts +1 -0
  66. package/dist/workflows.d.ts.map +1 -1
  67. package/dist/workflows.js +55 -23
  68. package/dist/workflows.js.map +1 -1
  69. package/package.json +9 -5
  70. package/src/approvals.ts +62 -45
  71. package/src/cli.ts +11 -2
  72. package/src/context.ts +32 -1
  73. package/src/diff.ts +40 -2
  74. package/src/handoffs.ts +20 -8
  75. package/src/host.ts +146 -23
  76. package/src/index.ts +7 -0
  77. package/src/isolated.ts +39 -2
  78. package/src/native.ts +213 -39
  79. package/src/orchestration/compile.ts +28 -2
  80. package/src/orchestration/evidence-store.ts +9 -3
  81. package/src/orchestration/host.ts +17 -0
  82. package/src/replay.ts +206 -25
  83. package/src/rpc.ts +262 -33
  84. package/src/session-migration.ts +3 -2
  85. package/src/sessions.ts +288 -57
  86. package/src/subagents.ts +147 -17
  87. package/src/verification.ts +146 -3
  88. 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
- this.session = await this.backend.createSession(input)
90
- let history: readonly import("./sessions.ts").SessionLogEntry[] = []
91
- if (input.sessionId !== undefined && this.sessionStore !== undefined) {
92
- history = await this.sessionStore.read(input.sessionId)
93
- this.context.restore(history.map(sessionEntryToContextRecord))
94
- } else {
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: AgentEvent): AgentEvent {
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 event = Object.freeze({
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
- this.session = Object.freeze({
211
- ...session,
212
- lastSeq: event.seq,
213
- updatedAt: event.at,
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
- await this.sessionStore?.append(id, "session.stopped", { reason })
322
- await this.backend.cancel(id, reason).catch(() => {})
323
- await this.backend.close(id).catch(() => {})
324
- await this.extensions?.close().catch(() => {})
325
- this.approvals.close()
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
- for (const name of ["PATH", "HOME", "LANG", "LC_ALL", "TERM"]) {
340
- const value = process.env[name]
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
+ }