@navels/neal 0.1.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 (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +527 -0
  3. package/SECURITY.md +91 -0
  4. package/config.yml +104 -0
  5. package/dist/neal/activity-footer.js +177 -0
  6. package/dist/neal/activity-reporting.js +1 -0
  7. package/dist/neal/adjudicator/artifacts.js +58 -0
  8. package/dist/neal/adjudicator/blocked-adjudicator.js +223 -0
  9. package/dist/neal/adjudicator/contracts.js +139 -0
  10. package/dist/neal/adjudicator/execute.js +611 -0
  11. package/dist/neal/adjudicator/final-completion.js +104 -0
  12. package/dist/neal/adjudicator/planning.js +145 -0
  13. package/dist/neal/adjudicator/specs.js +453 -0
  14. package/dist/neal/agents/prompts.js +120 -0
  15. package/dist/neal/agents/rounds.js +706 -0
  16. package/dist/neal/agents/schemas.js +832 -0
  17. package/dist/neal/agents/structured-coder.js +82 -0
  18. package/dist/neal/agents/structured-json.js +528 -0
  19. package/dist/neal/agents.js +4 -0
  20. package/dist/neal/atomic-write.js +18 -0
  21. package/dist/neal/blocked-guidance.js +406 -0
  22. package/dist/neal/cli.js +471 -0
  23. package/dist/neal/commands/check.js +401 -0
  24. package/dist/neal/commands/compat.js +807 -0
  25. package/dist/neal/commands/interactive-activity.js +57 -0
  26. package/dist/neal/commands/new-run.js +79 -0
  27. package/dist/neal/commands/plan-and-execute.js +44 -0
  28. package/dist/neal/commands/recovery-guidance.js +217 -0
  29. package/dist/neal/commands/resume-run.js +395 -0
  30. package/dist/neal/commands/review.js +21 -0
  31. package/dist/neal/commands/runtime.js +557 -0
  32. package/dist/neal/commands/setup.js +596 -0
  33. package/dist/neal/commands/squash.js +113 -0
  34. package/dist/neal/commands/status.js +33 -0
  35. package/dist/neal/commands/writer-exit-codes.js +42 -0
  36. package/dist/neal/commit-message.js +17 -0
  37. package/dist/neal/config.js +432 -0
  38. package/dist/neal/context/artifacts.js +140 -0
  39. package/dist/neal/context/context.js +324 -0
  40. package/dist/neal/context/inline-review-context.js +131 -0
  41. package/dist/neal/context/reviewer-context.js +166 -0
  42. package/dist/neal/context/shared.js +117 -0
  43. package/dist/neal/context/types.js +1 -0
  44. package/dist/neal/diagnostic.js +208 -0
  45. package/dist/neal/execute-finalization.js +5 -0
  46. package/dist/neal/final-completion-review.js +188 -0
  47. package/dist/neal/final-completion.js +229 -0
  48. package/dist/neal/git.js +339 -0
  49. package/dist/neal/index.js +135 -0
  50. package/dist/neal/interactive-controls.js +85 -0
  51. package/dist/neal/logger.js +102 -0
  52. package/dist/neal/manual-gates.js +121 -0
  53. package/dist/neal/orchestrator/artifacts.js +70 -0
  54. package/dist/neal/orchestrator/completion.js +531 -0
  55. package/dist/neal/orchestrator/failures.js +31 -0
  56. package/dist/neal/orchestrator/notifications.js +175 -0
  57. package/dist/neal/orchestrator/phases/coder.js +516 -0
  58. package/dist/neal/orchestrator/phases/planning.js +540 -0
  59. package/dist/neal/orchestrator/phases/recovery.js +798 -0
  60. package/dist/neal/orchestrator/phases/review.js +136 -0
  61. package/dist/neal/orchestrator/phases/shared.js +279 -0
  62. package/dist/neal/orchestrator/run-loop.js +113 -0
  63. package/dist/neal/orchestrator/split-plan.js +235 -0
  64. package/dist/neal/orchestrator/transitions.js +309 -0
  65. package/dist/neal/orchestrator.js +215 -0
  66. package/dist/neal/phase-display.js +27 -0
  67. package/dist/neal/plan-doc.js +154 -0
  68. package/dist/neal/plan-queue.js +1092 -0
  69. package/dist/neal/plan-refinement.js +39 -0
  70. package/dist/neal/plan-validation.js +525 -0
  71. package/dist/neal/progress.js +237 -0
  72. package/dist/neal/prompts/assert-builder.js +13 -0
  73. package/dist/neal/prompts/execute.js +290 -0
  74. package/dist/neal/prompts/guidance.js +70 -0
  75. package/dist/neal/prompts/planning.js +313 -0
  76. package/dist/neal/prompts/review-doctrine.js +142 -0
  77. package/dist/neal/prompts/shared.js +101 -0
  78. package/dist/neal/prompts/specialized.js +212 -0
  79. package/dist/neal/prompts/specs.js +572 -0
  80. package/dist/neal/providers/anthropic-claude.js +1599 -0
  81. package/dist/neal/providers/detection.js +139 -0
  82. package/dist/neal/providers/generic-agentic-tools.js +586 -0
  83. package/dist/neal/providers/generic-agentic.js +1238 -0
  84. package/dist/neal/providers/liveness.js +151 -0
  85. package/dist/neal/providers/openai-codex.js +1014 -0
  86. package/dist/neal/providers/openai-compatible.js +654 -0
  87. package/dist/neal/providers/registry.js +389 -0
  88. package/dist/neal/providers/telemetry.js +208 -0
  89. package/dist/neal/providers/types.js +21 -0
  90. package/dist/neal/recovery-artifacts.js +50 -0
  91. package/dist/neal/resume-decision.js +220 -0
  92. package/dist/neal/resume-planner.js +265 -0
  93. package/dist/neal/retrospective.js +391 -0
  94. package/dist/neal/review-debt.js +18 -0
  95. package/dist/neal/review-findings/artifacts.js +173 -0
  96. package/dist/neal/review-findings/prompts.js +172 -0
  97. package/dist/neal/review-findings/provider.js +330 -0
  98. package/dist/neal/review-findings/run.js +373 -0
  99. package/dist/neal/review-findings/types.js +1 -0
  100. package/dist/neal/review-mode.js +67 -0
  101. package/dist/neal/review.js +137 -0
  102. package/dist/neal/run-lock.js +334 -0
  103. package/dist/neal/run-metrics.js +355 -0
  104. package/dist/neal/run-narrative-types.js +1 -0
  105. package/dist/neal/run-narrative.js +1374 -0
  106. package/dist/neal/run-registry.js +218 -0
  107. package/dist/neal/run-status.js +25 -0
  108. package/dist/neal/scopes.js +451 -0
  109. package/dist/neal/sensitive-text.js +8 -0
  110. package/dist/neal/squash-message.js +379 -0
  111. package/dist/neal/squash.js +591 -0
  112. package/dist/neal/state-invariants.js +496 -0
  113. package/dist/neal/state-views.js +344 -0
  114. package/dist/neal/state.js +887 -0
  115. package/dist/neal/status-footer.js +258 -0
  116. package/dist/neal/status.js +1260 -0
  117. package/dist/neal/storage-paths.js +57 -0
  118. package/dist/neal/support.js +58 -0
  119. package/dist/neal/terminal-narrator.js +435 -0
  120. package/dist/neal/types.js +1 -0
  121. package/dist/neal/verification-events.js +81 -0
  122. package/dist/neal/version.js +37 -0
  123. package/dist/neal/worktree-status.js +137 -0
  124. package/dist/notifier.js +44 -0
  125. package/docs/ADJUDICATOR_INVENTORY.md +310 -0
  126. package/docs/PROMPT_SPECS.md +266 -0
  127. package/docs/README.md +22 -0
  128. package/docs/architecture.md +113 -0
  129. package/docs/assets/neal-execution-flow.png +0 -0
  130. package/docs/automation.md +65 -0
  131. package/docs/comparison.md +105 -0
  132. package/docs/compat.md +269 -0
  133. package/docs/compatible-models.md +135 -0
  134. package/docs/demo.md +55 -0
  135. package/docs/maintenance.md +64 -0
  136. package/docs/plan-format.md +213 -0
  137. package/docs/providers.md +751 -0
  138. package/docs/release.md +147 -0
  139. package/docs/state-machine.md +266 -0
  140. package/docs/storage.md +207 -0
  141. package/docs/troubleshooting.md +152 -0
  142. package/examples/compat/add-edit-verify/PLAN.md +29 -0
  143. package/examples/compat/add-edit-verify/broken.diff +8 -0
  144. package/examples/compat/add-edit-verify/good.diff +8 -0
  145. package/examples/compat/add-edit-verify/package.json +5 -0
  146. package/examples/compat/add-edit-verify/src/add.js +2 -0
  147. package/examples/compat/add-edit-verify/test/add.test.js +9 -0
  148. package/examples/compat/is-even-add-test/PLAN.md +30 -0
  149. package/examples/compat/is-even-add-test/broken.diff +11 -0
  150. package/examples/compat/is-even-add-test/good.diff +11 -0
  151. package/examples/compat/is-even-add-test/package.json +5 -0
  152. package/examples/compat/is-even-add-test/src/is-even.js +3 -0
  153. package/examples/compat/is-even-add-test/test/is-even.test.js +9 -0
  154. package/examples/compat/manifest.json +60 -0
  155. package/examples/compat/plan-greeting/ISSUE.md +25 -0
  156. package/examples/compat/plan-greeting/package.json +5 -0
  157. package/examples/compat/plan-greeting/src/greet.js +2 -0
  158. package/examples/compat/plan-greeting/test/greet.test.js +8 -0
  159. package/examples/compat/reverse-grep-edit/PLAN.md +32 -0
  160. package/examples/compat/reverse-grep-edit/broken.diff +12 -0
  161. package/examples/compat/reverse-grep-edit/good.diff +12 -0
  162. package/examples/compat/reverse-grep-edit/package.json +5 -0
  163. package/examples/compat/reverse-grep-edit/src/strings.js +10 -0
  164. package/examples/compat/reverse-grep-edit/test/strings.test.js +15 -0
  165. package/examples/issue-triage-js/PLAN.md +83 -0
  166. package/examples/issue-triage-js/README.md +76 -0
  167. package/examples/issue-triage-js/package.json +9 -0
  168. package/examples/issue-triage-js/src/issue-triage.js +87 -0
  169. package/examples/issue-triage-js/test/issue-triage.test.js +107 -0
  170. package/package.json +70 -0
@@ -0,0 +1,82 @@
1
+ import { getAgentTurnRetryLimit, getAgentTurnStartupTimeoutMs, getApiRetryLimit, getInactivityTimeoutMs } from '../config.js';
2
+ import { runWithAgentTurnLiveness } from '../providers/liveness.js';
3
+ import { getCoderAdapter } from '../providers/registry.js';
4
+ import { createProviderTelemetrySink } from '../providers/telemetry.js';
5
+ import { isNealProviderError } from '../providers/types.js';
6
+ export class CoderRoundError extends Error {
7
+ sessionHandle;
8
+ providerError;
9
+ kind;
10
+ retryable;
11
+ constructor(providerError) {
12
+ super(providerError.message);
13
+ this.name = 'CoderRoundError';
14
+ this.sessionHandle = providerError.sessionHandle;
15
+ this.providerError = providerError;
16
+ this.kind = providerError.kind;
17
+ this.retryable = providerError.retryable;
18
+ }
19
+ }
20
+ export function translateCoderProviderError(error) {
21
+ if (isNealProviderError(error)) {
22
+ return new CoderRoundError(error);
23
+ }
24
+ return error;
25
+ }
26
+ function getCoderRuntimeOptions(cwd) {
27
+ return {
28
+ inactivityTimeoutMs: getInactivityTimeoutMs(cwd),
29
+ };
30
+ }
31
+ function createCoderProviderEventSink(args) {
32
+ return createProviderTelemetrySink({
33
+ logger: args.logger,
34
+ provider: args.coder.provider,
35
+ role: 'coder',
36
+ ...(args.label ? { label: args.label } : {}),
37
+ cwd: args.cwd,
38
+ });
39
+ }
40
+ export async function runCoderStructuredPrompt(args) {
41
+ try {
42
+ const coder = getCoderAdapter(args.coder);
43
+ return await runWithAgentTurnLiveness({
44
+ provider: args.coder.provider,
45
+ role: 'coder',
46
+ ...(args.telemetryLabel ? { label: args.telemetryLabel } : {}),
47
+ startupTimeoutMs: Math.min(getAgentTurnStartupTimeoutMs(args.cwd), getInactivityTimeoutMs(args.cwd)),
48
+ // Resumed sessions never retry in the supervisor; the orchestrator's
49
+ // fresh-session retry owns recovery so session-handle state stays
50
+ // consistent.
51
+ retryLimit: args.resumeHandle ? 0 : getAgentTurnRetryLimit(args.cwd),
52
+ logger: args.logger,
53
+ baseSink: createCoderProviderEventSink({
54
+ coder: args.coder,
55
+ cwd: args.cwd,
56
+ label: args.telemetryLabel,
57
+ logger: args.logger,
58
+ }),
59
+ run: (events, attempt) => coder.runStructuredPrompt({
60
+ cwd: args.cwd,
61
+ prompt: args.prompt,
62
+ label: args.label,
63
+ schema: args.schema,
64
+ structuredJsonProtocol: args.structuredJsonProtocol,
65
+ toolPolicy: args.toolPolicy,
66
+ ...getCoderRuntimeOptions(args.cwd),
67
+ // Resumed sessions never retry in-round; the orchestrator's
68
+ // fresh-session retry owns recovery.
69
+ apiRetryLimit: args.resumeHandle ? 0 : getApiRetryLimit(args.cwd),
70
+ resumeHandle: args.resumeHandle,
71
+ // Guarded so an abandoned attempt's late session handle can never
72
+ // overwrite a newer attempt's handle.
73
+ onSessionStarted: attempt.guard(args.onSessionStarted),
74
+ signal: attempt.signal,
75
+ events,
76
+ }),
77
+ });
78
+ }
79
+ catch (error) {
80
+ throw translateCoderProviderError(error);
81
+ }
82
+ }
@@ -0,0 +1,528 @@
1
+ import { NealProviderError } from '../providers/types.js';
2
+ const SUMMARY_MAX_LENGTH = 800;
3
+ const ORIGINAL_RESPONSE_MAX_LENGTH = 12000;
4
+ const NEAL_JSON_OPENING_FENCE_PATTERN = /^[ \t]*```[ \t]*neal-json[ \t]*$/;
5
+ const CLOSING_FENCE_PATTERN = /^[ \t]*```[ \t]*$/;
6
+ export function buildStructuredJsonPrompt(basePrompt, protocol) {
7
+ const schemaDescription = typeof protocol.schema === 'string' ? protocol.schema : JSON.stringify(protocol.schema, null, 2);
8
+ const allowProseBeforeBlock = protocol.allowProseBeforeBlock ?? true;
9
+ const lines = [
10
+ basePrompt.trimEnd(),
11
+ '',
12
+ 'Neal structured control protocol:',
13
+ `- Protocol: ${protocol.protocol}`,
14
+ `- Schema label: ${protocol.schemaLabel}`,
15
+ allowProseBeforeBlock
16
+ ? '- You may include concise useful prose before the control payload.'
17
+ : '- Do not include prose outside the control payload.',
18
+ '- End your response with exactly one final fenced ```neal-json JSON block.',
19
+ '- No non-whitespace content may appear after the closing fence.',
20
+ '- The fenced JSON must be a single JSON object that satisfies this schema or schema summary:',
21
+ fence(schemaDescription, 'json'),
22
+ ];
23
+ if (protocol.responseShapeHint?.trim()) {
24
+ lines.push('', 'Response shape guidance:', protocol.responseShapeHint.trim());
25
+ }
26
+ if (protocol.examplePayload !== undefined) {
27
+ lines.push('', 'Example control payload:', fence(JSON.stringify(protocol.examplePayload, null, 2), 'json'));
28
+ }
29
+ return lines.join('\n');
30
+ }
31
+ export function extractStructuredJsonPayload(assistantText) {
32
+ const blocks = findNealJsonBlocks(assistantText);
33
+ if (blocks.length > 1) {
34
+ return {
35
+ ok: false,
36
+ errorKind: 'multiple_control_blocks',
37
+ errorSummary: `Expected exactly one final neal-json control block, but found ${blocks.length}.`,
38
+ prose: assistantText.trim(),
39
+ rawJson: null,
40
+ };
41
+ }
42
+ if (blocks.length === 1) {
43
+ const block = blocks[0];
44
+ const trailing = assistantText.slice(block.end);
45
+ const prose = assistantText.slice(0, block.start).trim();
46
+ if (trailing.trim().length > 0) {
47
+ return {
48
+ ok: false,
49
+ errorKind: 'non_final_control_block',
50
+ errorSummary: 'The neal-json control block must be the final non-whitespace content.',
51
+ prose,
52
+ rawJson: block.rawJson,
53
+ };
54
+ }
55
+ return parseExtractedJson({
56
+ source: 'neal-json-block',
57
+ prose,
58
+ rawJson: block.rawJson,
59
+ malformedSummaryPrefix: 'The neal-json control block contained malformed JSON',
60
+ });
61
+ }
62
+ const trimmed = assistantText.trim();
63
+ if (!looksLikeRawJsonObject(trimmed)) {
64
+ return {
65
+ ok: false,
66
+ errorKind: 'missing_control_block',
67
+ errorSummary: 'Expected a final neal-json control block or a raw whole-response JSON object.',
68
+ prose: trimmed,
69
+ rawJson: null,
70
+ };
71
+ }
72
+ // Compatibility tolerance for pre-migration/local mocks that return only a
73
+ // JSON object. The prompt contract still asks providers for a final
74
+ // fenced neal-json block, and docs should describe raw JSON as tolerated,
75
+ // not preferred.
76
+ return parseExtractedJson({
77
+ source: 'raw-json',
78
+ prose: '',
79
+ rawJson: trimmed,
80
+ malformedSummaryPrefix: 'The raw JSON response was invalid',
81
+ });
82
+ }
83
+ export function validateStructuredJsonPayload(assistantText, validator) {
84
+ const extraction = extractStructuredJsonPayload(assistantText);
85
+ if (!extraction.ok) {
86
+ return {
87
+ ok: false,
88
+ phase: 'extraction',
89
+ errorKind: extraction.errorKind,
90
+ errorSummary: extraction.errorSummary,
91
+ extraction,
92
+ };
93
+ }
94
+ try {
95
+ return {
96
+ ...extraction,
97
+ structured: validator(extraction.payload),
98
+ };
99
+ }
100
+ catch (error) {
101
+ return {
102
+ ok: false,
103
+ phase: 'validation',
104
+ errorKind: 'schema_invalid',
105
+ errorSummary: `Extracted JSON failed schema validation: ${summarizeError(error)}`,
106
+ extraction,
107
+ };
108
+ }
109
+ }
110
+ export function buildStructuredJsonRepairPrompt(args) {
111
+ const issueSummaries = [
112
+ args.extractionErrorSummary ? `Extraction or parse issue: ${args.extractionErrorSummary}` : null,
113
+ args.validationErrorSummary ? `Validation issue: ${args.validationErrorSummary}` : null,
114
+ ].filter((line) => line !== null);
115
+ const schemaDescription = typeof args.schema === 'string' ? args.schema : JSON.stringify(args.schema, null, 2);
116
+ const invalidJsonSection = args.invalidJson === undefined || args.invalidJson === null
117
+ ? 'No JSON payload was extracted.'
118
+ : fence(args.invalidJson, 'json');
119
+ return [
120
+ `You are repairing a Neal structured control payload for schema "${args.schemaLabel}".`,
121
+ `Repair attempt ${args.attemptNumber} of ${args.attemptLimit}.`,
122
+ '',
123
+ 'Preserve the original control decision and substantive meaning. Do not inspect the repository, run tools, request more context, or invent new implementation or review work.',
124
+ 'Return only a valid repaired response: either raw whole-response JSON object, or prose followed by exactly one final fenced ```neal-json control block. No content may appear after the closing fence.',
125
+ '',
126
+ 'The JSON object must satisfy this schema or schema summary:',
127
+ fence(schemaDescription, 'json'),
128
+ '',
129
+ 'Repair issue summary:',
130
+ issueSummaries.length > 0 ? issueSummaries.join('\n') : 'The previous response did not produce a valid structured JSON payload.',
131
+ '',
132
+ 'Extracted invalid JSON, if any:',
133
+ invalidJsonSection,
134
+ '',
135
+ 'Original assistant response:',
136
+ fence(truncateForPrompt(args.originalAssistantText), 'text'),
137
+ ].join('\n');
138
+ }
139
+ export async function runStructuredJsonProtocol(args) {
140
+ const wrappedPrompt = buildStructuredJsonPrompt(args.prompt, args.protocol);
141
+ const initial = await args.runInitial(wrappedPrompt);
142
+ const initialSessionHandle = initial.sessionHandle ?? args.initialSessionHandle ?? null;
143
+ await emitStructuredJsonExtractionStarted({
144
+ ...args,
145
+ assistantText: initial.assistantText,
146
+ sessionHandle: initialSessionHandle,
147
+ attemptNumber: 0,
148
+ });
149
+ const initialValidation = validateStructuredJsonPayload(initial.assistantText, args.protocol.validator);
150
+ if (initialValidation.ok) {
151
+ await emitStructuredOutputReceived({
152
+ ...args,
153
+ validation: initialValidation,
154
+ assistantTextLength: initial.assistantText.length,
155
+ sessionHandle: initialSessionHandle,
156
+ attemptNumber: 0,
157
+ });
158
+ return {
159
+ sessionHandle: initialSessionHandle,
160
+ structured: initialValidation.structured,
161
+ };
162
+ }
163
+ await emitStructuredJsonFailure({
164
+ ...args,
165
+ failure: initialValidation,
166
+ assistantTextLength: initial.assistantText.length,
167
+ sessionHandle: initialSessionHandle,
168
+ attemptNumber: 0,
169
+ });
170
+ let repairResult;
171
+ try {
172
+ repairResult = await runStructuredJsonRepairLoop({
173
+ originalAssistantText: initial.assistantText,
174
+ initialFailure: initialValidation,
175
+ schemaLabel: args.protocol.schemaLabel,
176
+ schema: args.protocol.schema,
177
+ attemptLimit: args.protocol.repairAttemptLimit,
178
+ validator: args.protocol.validator,
179
+ callRepair: async ({ prompt, attemptNumber, attemptLimit }) => {
180
+ await emitStructuredJsonRepairStarted({
181
+ ...args,
182
+ assistantTextLength: initial.assistantText.length,
183
+ sessionHandle: initialSessionHandle,
184
+ attemptNumber,
185
+ attemptLimit,
186
+ });
187
+ return args.runRepair(prompt, attemptNumber);
188
+ },
189
+ });
190
+ }
191
+ catch (error) {
192
+ await emitStructuredJsonRepairFailed({
193
+ ...args,
194
+ failure: initialValidation,
195
+ assistantTextLength: initial.assistantText.length,
196
+ sessionHandle: initialSessionHandle,
197
+ attemptNumber: 0,
198
+ message: 'Structured JSON repair failed before producing a valid Neal payload.',
199
+ cause: error,
200
+ });
201
+ throw error;
202
+ }
203
+ for (const attempt of repairResult.attempts) {
204
+ if (!attempt.result.ok) {
205
+ await emitStructuredJsonFailure({
206
+ ...args,
207
+ failure: attempt.result,
208
+ assistantTextLength: initial.assistantText.length,
209
+ sessionHandle: initialSessionHandle ?? attempt.sessionHandle,
210
+ attemptNumber: attempt.attemptNumber,
211
+ repairSessionHandle: attempt.sessionHandle,
212
+ });
213
+ }
214
+ }
215
+ if (repairResult.ok) {
216
+ const returnedSessionHandle = initialSessionHandle ?? repairResult.sessionHandle;
217
+ await emitStructuredJsonRepairSucceeded({
218
+ ...args,
219
+ validation: repairResult.result,
220
+ assistantTextLength: initial.assistantText.length,
221
+ sessionHandle: returnedSessionHandle,
222
+ attemptNumber: repairResult.attemptNumber,
223
+ repairSessionHandle: repairResult.sessionHandle,
224
+ });
225
+ await emitStructuredOutputReceived({
226
+ ...args,
227
+ validation: repairResult.result,
228
+ assistantTextLength: initial.assistantText.length,
229
+ sessionHandle: returnedSessionHandle,
230
+ attemptNumber: repairResult.attemptNumber,
231
+ repairSessionHandle: repairResult.sessionHandle,
232
+ });
233
+ return {
234
+ sessionHandle: returnedSessionHandle,
235
+ structured: repairResult.result.structured,
236
+ };
237
+ }
238
+ await emitStructuredJsonRepairFailed({
239
+ ...args,
240
+ failure: repairResult.lastFailure,
241
+ assistantTextLength: initial.assistantText.length,
242
+ sessionHandle: initialSessionHandle,
243
+ attemptNumber: repairResult.attempts.at(-1)?.attemptNumber ?? args.protocol.repairAttemptLimit,
244
+ repairSessionHandle: repairResult.attempts.at(-1)?.sessionHandle ?? null,
245
+ message: 'Structured JSON repair attempts were exhausted.',
246
+ });
247
+ throw buildStructuredJsonProviderError(args, {
248
+ message: `Neal structured control payload remained invalid after ${args.protocol.repairAttemptLimit} repair attempt(s): ${repairResult.lastFailure.errorSummary}`,
249
+ kind: 'structured_output_invalid',
250
+ sessionHandle: initialSessionHandle,
251
+ cause: repairResult.lastFailure,
252
+ });
253
+ }
254
+ export async function runStructuredJsonRepairLoop(args) {
255
+ if (!Number.isSafeInteger(args.attemptLimit) || args.attemptLimit < 1) {
256
+ throw new Error('Structured JSON repair attemptLimit must be a positive safe integer.');
257
+ }
258
+ const attempts = [];
259
+ let lastFailure = args.initialFailure;
260
+ for (let attemptNumber = 1; attemptNumber <= args.attemptLimit; attemptNumber += 1) {
261
+ const prompt = buildStructuredJsonRepairPrompt({
262
+ originalAssistantText: args.originalAssistantText,
263
+ invalidJson: getInvalidJson(lastFailure),
264
+ extractionErrorSummary: lastFailure.phase === 'extraction' ? lastFailure.errorSummary : null,
265
+ validationErrorSummary: lastFailure.phase === 'validation' ? lastFailure.errorSummary : null,
266
+ schemaLabel: args.schemaLabel,
267
+ schema: args.schema,
268
+ attemptNumber,
269
+ attemptLimit: args.attemptLimit,
270
+ });
271
+ const repairResponse = normalizeRepairCallResult(await args.callRepair({
272
+ prompt,
273
+ attemptNumber,
274
+ attemptLimit: args.attemptLimit,
275
+ }));
276
+ const result = validateStructuredJsonPayload(repairResponse.assistantText, args.validator);
277
+ const attempt = {
278
+ attemptNumber,
279
+ sessionHandle: repairResponse.sessionHandle,
280
+ result,
281
+ };
282
+ attempts.push(attempt);
283
+ if (result.ok) {
284
+ return {
285
+ ok: true,
286
+ attemptNumber,
287
+ sessionHandle: repairResponse.sessionHandle,
288
+ result,
289
+ attempts,
290
+ };
291
+ }
292
+ lastFailure = result;
293
+ }
294
+ return {
295
+ ok: false,
296
+ attempts,
297
+ lastFailure,
298
+ };
299
+ }
300
+ function buildStructuredJsonProviderError(args, errorArgs) {
301
+ if (args.createProviderError) {
302
+ return args.createProviderError(errorArgs);
303
+ }
304
+ return new NealProviderError({
305
+ message: errorArgs.message,
306
+ provider: args.provider,
307
+ role: args.role,
308
+ sessionHandle: errorArgs.sessionHandle,
309
+ kind: errorArgs.kind,
310
+ retryable: false,
311
+ cause: errorArgs.cause,
312
+ });
313
+ }
314
+ async function emitStructuredJsonExtractionStarted(args) {
315
+ await emitStructuredJsonEvent(args.events, {
316
+ type: 'tool_progress',
317
+ provider: args.provider,
318
+ role: args.role,
319
+ label: args.label,
320
+ sessionHandle: args.sessionHandle,
321
+ toolName: 'structured_json_extraction_started',
322
+ message: 'Extracting Neal structured control payload.',
323
+ providerData: buildStructuredJsonProtocolProviderData(args),
324
+ });
325
+ }
326
+ async function emitStructuredJsonFailure(args) {
327
+ await emitStructuredJsonEvent(args.events, {
328
+ type: 'tool_progress',
329
+ provider: args.provider,
330
+ role: args.role,
331
+ label: args.label,
332
+ sessionHandle: args.sessionHandle,
333
+ toolName: args.failure.phase === 'validation'
334
+ ? 'structured_json_validation_failed'
335
+ : 'structured_json_extraction_failed',
336
+ message: args.failure.phase === 'validation'
337
+ ? 'Neal structured control payload failed validation.'
338
+ : 'Neal structured control payload could not be extracted or parsed.',
339
+ isError: true,
340
+ providerData: buildStructuredJsonProtocolProviderData(args),
341
+ });
342
+ }
343
+ async function emitStructuredJsonRepairStarted(args) {
344
+ await emitStructuredJsonEvent(args.events, {
345
+ type: 'tool_progress',
346
+ provider: args.provider,
347
+ role: args.role,
348
+ label: args.label,
349
+ sessionHandle: args.sessionHandle,
350
+ toolName: 'structured_json_repair_started',
351
+ message: `Repairing Neal structured control payload (${args.attemptNumber}/${args.attemptLimit}).`,
352
+ providerData: {
353
+ ...buildStructuredJsonProtocolProviderData(args),
354
+ attemptLimit: args.attemptLimit,
355
+ },
356
+ });
357
+ }
358
+ async function emitStructuredJsonRepairSucceeded(args) {
359
+ await emitStructuredJsonEvent(args.events, {
360
+ type: 'tool_progress',
361
+ provider: args.provider,
362
+ role: args.role,
363
+ label: args.label,
364
+ sessionHandle: args.sessionHandle,
365
+ toolName: 'structured_json_repair_succeeded',
366
+ message: 'Structured JSON repair produced a valid Neal payload.',
367
+ providerData: buildStructuredJsonProtocolProviderData(args),
368
+ });
369
+ }
370
+ async function emitStructuredJsonRepairFailed(args) {
371
+ await emitStructuredJsonEvent(args.events, {
372
+ type: 'tool_progress',
373
+ provider: args.provider,
374
+ role: args.role,
375
+ label: args.label,
376
+ sessionHandle: args.sessionHandle,
377
+ toolName: 'structured_json_repair_failed',
378
+ message: args.message,
379
+ isError: true,
380
+ providerData: {
381
+ ...buildStructuredJsonProtocolProviderData(args),
382
+ ...(args.cause instanceof Error ? { errorMessage: args.cause.message } : {}),
383
+ },
384
+ });
385
+ }
386
+ async function emitStructuredOutputReceived(args) {
387
+ await emitStructuredJsonEvent(args.events, {
388
+ type: 'structured_output_received',
389
+ provider: args.provider,
390
+ role: args.role,
391
+ label: args.label,
392
+ sessionHandle: args.sessionHandle,
393
+ providerData: buildStructuredJsonProtocolProviderData(args),
394
+ });
395
+ }
396
+ async function emitStructuredJsonEvent(events, event) {
397
+ await events?.(event);
398
+ }
399
+ function buildStructuredJsonProtocolProviderData(args) {
400
+ return {
401
+ protocol: args.protocol.protocol,
402
+ schemaLabel: args.protocol.schemaLabel,
403
+ attemptNumber: args.attemptNumber,
404
+ repairAttemptLimit: args.protocol.repairAttemptLimit,
405
+ assistantTextLength: args.assistantTextLength ?? args.assistantText?.length ?? 0,
406
+ extractedJsonLength: args.validation?.rawJson.length ?? args.failure?.extraction.rawJson?.length ?? 0,
407
+ ...(args.validation ? { source: args.validation.source } : {}),
408
+ ...(args.failure?.phase === 'extraction' ? { parseErrorSummary: args.failure.errorSummary } : {}),
409
+ ...(args.failure?.phase === 'validation' ? { validationErrorSummary: args.failure.errorSummary } : {}),
410
+ ...(args.initialSessionHandle !== undefined ? { originalSessionHandle: args.initialSessionHandle } : {}),
411
+ ...(args.repairSessionHandle !== undefined ? { repairSessionHandle: args.repairSessionHandle } : {}),
412
+ };
413
+ }
414
+ function findNealJsonBlocks(assistantText) {
415
+ const blocks = [];
416
+ const lines = splitLinesWithOffsets(assistantText);
417
+ for (let index = 0; index < lines.length; index += 1) {
418
+ const openingLine = lines[index];
419
+ if (!NEAL_JSON_OPENING_FENCE_PATTERN.test(openingLine.text)) {
420
+ continue;
421
+ }
422
+ for (let closingIndex = index + 1; closingIndex < lines.length; closingIndex += 1) {
423
+ const closingLine = lines[closingIndex];
424
+ if (!CLOSING_FENCE_PATTERN.test(closingLine.text)) {
425
+ continue;
426
+ }
427
+ blocks.push({
428
+ start: openingLine.start,
429
+ end: closingLine.end,
430
+ rawJson: stripSingleTrailingLineBreak(assistantText.slice(openingLine.end, closingLine.start)),
431
+ });
432
+ index = closingIndex;
433
+ break;
434
+ }
435
+ }
436
+ return blocks;
437
+ }
438
+ function splitLinesWithOffsets(text) {
439
+ const lines = [];
440
+ let start = 0;
441
+ while (start < text.length) {
442
+ const newlineIndex = text.indexOf('\n', start);
443
+ const end = newlineIndex === -1 ? text.length : newlineIndex + 1;
444
+ const rawLine = text.slice(start, end);
445
+ const textWithoutLineBreak = rawLine.replace(/\r?\n$/, '');
446
+ lines.push({
447
+ text: textWithoutLineBreak,
448
+ start,
449
+ end,
450
+ });
451
+ start = end;
452
+ }
453
+ return lines;
454
+ }
455
+ function stripSingleTrailingLineBreak(value) {
456
+ return value.replace(/\r?\n$/, '');
457
+ }
458
+ function parseExtractedJson(args) {
459
+ try {
460
+ const payload = JSON.parse(args.rawJson);
461
+ if (!isJsonObject(payload)) {
462
+ return {
463
+ ok: false,
464
+ errorKind: 'malformed_json',
465
+ errorSummary: `${args.malformedSummaryPrefix}: expected a single JSON object.`,
466
+ prose: args.prose,
467
+ rawJson: args.rawJson,
468
+ };
469
+ }
470
+ return {
471
+ ok: true,
472
+ source: args.source,
473
+ prose: args.prose,
474
+ rawJson: args.rawJson,
475
+ payload,
476
+ };
477
+ }
478
+ catch (error) {
479
+ return {
480
+ ok: false,
481
+ errorKind: 'malformed_json',
482
+ errorSummary: `${args.malformedSummaryPrefix}: ${summarizeError(error)}`,
483
+ prose: args.prose,
484
+ rawJson: args.rawJson,
485
+ };
486
+ }
487
+ }
488
+ function looksLikeRawJsonObject(trimmed) {
489
+ return trimmed.startsWith('{') || trimmed.startsWith('[');
490
+ }
491
+ function isJsonObject(value) {
492
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
493
+ }
494
+ function normalizeRepairCallResult(result) {
495
+ if (typeof result === 'string') {
496
+ return {
497
+ assistantText: result,
498
+ sessionHandle: null,
499
+ };
500
+ }
501
+ return {
502
+ assistantText: result.assistantText,
503
+ sessionHandle: result.sessionHandle ?? null,
504
+ };
505
+ }
506
+ function getInvalidJson(failure) {
507
+ return failure.extraction.rawJson;
508
+ }
509
+ function summarizeError(error) {
510
+ const message = error instanceof Error ? error.message : String(error);
511
+ return limitOneLine(message, SUMMARY_MAX_LENGTH);
512
+ }
513
+ function limitOneLine(value, maxLength) {
514
+ const oneLine = value.replace(/\s+/g, ' ').trim();
515
+ if (oneLine.length <= maxLength) {
516
+ return oneLine;
517
+ }
518
+ return `${oneLine.slice(0, maxLength - 1)}…`;
519
+ }
520
+ function truncateForPrompt(value) {
521
+ if (value.length <= ORIGINAL_RESPONSE_MAX_LENGTH) {
522
+ return value;
523
+ }
524
+ return `${value.slice(0, ORIGINAL_RESPONSE_MAX_LENGTH)}\n[truncated ${value.length - ORIGINAL_RESPONSE_MAX_LENGTH} byte(s)]`;
525
+ }
526
+ function fence(value, language) {
527
+ return `\`\`\`${language}\n${value}\n\`\`\``;
528
+ }
@@ -0,0 +1,4 @@
1
+ export * from './agents/prompts.js';
2
+ export * from './agents/schemas.js';
3
+ export * from './agents/rounds.js';
4
+ export * from './agents/structured-coder.js';
@@ -0,0 +1,18 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdir, rename, rm, writeFile } from 'node:fs/promises';
3
+ import { basename, dirname, join } from 'node:path';
4
+ export async function writeJsonAtomic(path, value) {
5
+ await writeTextAtomic(path, JSON.stringify(value, null, 2) + '\n');
6
+ }
7
+ export async function writeTextAtomic(path, content) {
8
+ await mkdir(dirname(path), { recursive: true });
9
+ const tempPath = join(dirname(path), `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
10
+ try {
11
+ await writeFile(tempPath, content, 'utf8');
12
+ await rename(tempPath, path);
13
+ }
14
+ catch (error) {
15
+ await rm(tempPath, { force: true }).catch(() => undefined);
16
+ throw error;
17
+ }
18
+ }