agentxchain 0.8.8 → 2.1.1

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 (74) hide show
  1. package/README.md +126 -142
  2. package/bin/agentxchain.js +186 -5
  3. package/dashboard/app.js +305 -0
  4. package/dashboard/components/blocked.js +145 -0
  5. package/dashboard/components/cross-repo.js +126 -0
  6. package/dashboard/components/gate.js +311 -0
  7. package/dashboard/components/hooks.js +177 -0
  8. package/dashboard/components/initiative.js +147 -0
  9. package/dashboard/components/ledger.js +165 -0
  10. package/dashboard/components/timeline.js +222 -0
  11. package/dashboard/index.html +352 -0
  12. package/package.json +14 -6
  13. package/scripts/live-api-proxy-preflight-smoke.sh +531 -0
  14. package/scripts/publish-from-tag.sh +88 -0
  15. package/scripts/release-postflight.sh +231 -0
  16. package/scripts/release-preflight.sh +167 -0
  17. package/src/commands/accept-turn.js +160 -0
  18. package/src/commands/approve-completion.js +80 -0
  19. package/src/commands/approve-transition.js +85 -0
  20. package/src/commands/dashboard.js +70 -0
  21. package/src/commands/init.js +516 -0
  22. package/src/commands/migrate.js +348 -0
  23. package/src/commands/multi.js +549 -0
  24. package/src/commands/plugin.js +157 -0
  25. package/src/commands/reject-turn.js +204 -0
  26. package/src/commands/resume.js +389 -0
  27. package/src/commands/status.js +196 -3
  28. package/src/commands/step.js +947 -0
  29. package/src/commands/template-list.js +33 -0
  30. package/src/commands/template-set.js +279 -0
  31. package/src/commands/validate.js +20 -11
  32. package/src/commands/verify.js +71 -0
  33. package/src/lib/adapters/api-proxy-adapter.js +1076 -0
  34. package/src/lib/adapters/local-cli-adapter.js +337 -0
  35. package/src/lib/adapters/manual-adapter.js +169 -0
  36. package/src/lib/blocked-state.js +94 -0
  37. package/src/lib/config.js +97 -1
  38. package/src/lib/context-compressor.js +121 -0
  39. package/src/lib/context-section-parser.js +220 -0
  40. package/src/lib/coordinator-acceptance.js +428 -0
  41. package/src/lib/coordinator-config.js +461 -0
  42. package/src/lib/coordinator-dispatch.js +276 -0
  43. package/src/lib/coordinator-gates.js +487 -0
  44. package/src/lib/coordinator-hooks.js +239 -0
  45. package/src/lib/coordinator-recovery.js +523 -0
  46. package/src/lib/coordinator-state.js +365 -0
  47. package/src/lib/cross-repo-context.js +247 -0
  48. package/src/lib/dashboard/bridge-server.js +284 -0
  49. package/src/lib/dashboard/file-watcher.js +93 -0
  50. package/src/lib/dashboard/state-reader.js +96 -0
  51. package/src/lib/dispatch-bundle.js +568 -0
  52. package/src/lib/dispatch-manifest.js +252 -0
  53. package/src/lib/gate-evaluator.js +285 -0
  54. package/src/lib/governed-state.js +2139 -0
  55. package/src/lib/governed-templates.js +145 -0
  56. package/src/lib/hook-runner.js +788 -0
  57. package/src/lib/normalized-config.js +539 -0
  58. package/src/lib/plugin-config-schema.js +192 -0
  59. package/src/lib/plugins.js +692 -0
  60. package/src/lib/protocol-conformance.js +291 -0
  61. package/src/lib/reference-conformance-adapter.js +717 -0
  62. package/src/lib/repo-observer.js +597 -0
  63. package/src/lib/repo.js +0 -31
  64. package/src/lib/schema.js +121 -0
  65. package/src/lib/schemas/turn-result.schema.json +205 -0
  66. package/src/lib/token-budget.js +206 -0
  67. package/src/lib/token-counter.js +27 -0
  68. package/src/lib/turn-paths.js +67 -0
  69. package/src/lib/turn-result-validator.js +496 -0
  70. package/src/lib/validation.js +137 -0
  71. package/src/templates/governed/api-service.json +31 -0
  72. package/src/templates/governed/cli-tool.json +30 -0
  73. package/src/templates/governed/generic.json +10 -0
  74. package/src/templates/governed/web-app.json +30 -0
package/src/lib/schema.js CHANGED
@@ -29,6 +29,127 @@ export function validateStateSchema(data) {
29
29
  return { ok: errors.length === 0, errors };
30
30
  }
31
31
 
32
+ export function validateGovernedStateSchema(data) {
33
+ const errors = [];
34
+ const VALID_RUN_STATUSES = ['idle', 'active', 'paused', 'blocked', 'completed', 'failed'];
35
+ const isV1_1 = data?.schema_version === '1.1';
36
+ const hasLegacyCurrentTurn = Object.prototype.hasOwnProperty.call(data || {}, 'current_turn');
37
+
38
+ function validateTurn(turn, label) {
39
+ if (typeof turn !== 'object' || Array.isArray(turn)) {
40
+ errors.push(`${label} must be an object`);
41
+ return;
42
+ }
43
+
44
+ if (typeof turn.turn_id !== 'string' || !turn.turn_id.trim()) errors.push(`${label}.turn_id must be a non-empty string`);
45
+ if (typeof turn.assigned_role !== 'string' || !turn.assigned_role.trim()) errors.push(`${label}.assigned_role must be a non-empty string`);
46
+ if (typeof turn.status !== 'string' || !turn.status.trim()) errors.push(`${label}.status must be a non-empty string`);
47
+ if (typeof turn.runtime_id !== 'string' || !turn.runtime_id.trim()) errors.push(`${label}.runtime_id must be a non-empty string`);
48
+ if (typeof turn.attempt !== 'number' || !Number.isInteger(turn.attempt)) errors.push(`${label}.attempt must be an integer`);
49
+ if (isV1_1 && (!Number.isInteger(turn.assigned_sequence) || turn.assigned_sequence < 1)) {
50
+ errors.push(`${label}.assigned_sequence must be an integer >= 1`);
51
+ }
52
+ }
53
+
54
+ if (data === null || typeof data !== 'object') {
55
+ return { ok: false, errors: ['governed state must be a JSON object'] };
56
+ }
57
+
58
+ const hasRunId = typeof data.run_id === 'string' && data.run_id.trim();
59
+ const hasNoActiveTurn = isV1_1
60
+ ? data.active_turns && typeof data.active_turns === 'object' && !Array.isArray(data.active_turns)
61
+ ? Object.keys(data.active_turns).length === 0
62
+ : false
63
+ : data.current_turn === null;
64
+ const isUninitializedIdle = data.run_id === null && data.status === 'idle' && hasNoActiveTurn;
65
+ if (!hasRunId && !isUninitializedIdle) {
66
+ errors.push('run_id must be a non-empty string, or null only for an idle state with no active turn');
67
+ }
68
+ if (typeof data.project_id !== 'string' || !data.project_id.trim()) errors.push('project_id must be a non-empty string');
69
+ if (typeof data.status !== 'string' || !data.status.trim()) {
70
+ errors.push('status must be a non-empty string');
71
+ } else if (!VALID_RUN_STATUSES.includes(data.status)) {
72
+ errors.push(`status must be one of: ${VALID_RUN_STATUSES.join(', ')}`);
73
+ }
74
+ if (typeof data.phase !== 'string' || !data.phase.trim()) errors.push('phase must be a non-empty string');
75
+
76
+ if (isV1_1) {
77
+ if (hasLegacyCurrentTurn) {
78
+ errors.push('current_turn is not allowed when schema_version is "1.1"; use active_turns');
79
+ }
80
+ if (!data.active_turns || typeof data.active_turns !== 'object' || Array.isArray(data.active_turns)) {
81
+ errors.push('active_turns must be an object when schema_version is "1.1"');
82
+ } else {
83
+ for (const [turnId, turn] of Object.entries(data.active_turns)) {
84
+ validateTurn(turn, `active_turns.${turnId}`);
85
+ }
86
+ }
87
+ if (!Number.isInteger(data.turn_sequence) || data.turn_sequence < 0) {
88
+ errors.push('turn_sequence must be an integer >= 0 when schema_version is "1.1"');
89
+ }
90
+ } else if ('current_turn' in data && data.current_turn !== null) {
91
+ validateTurn(data.current_turn, 'current_turn');
92
+ }
93
+
94
+ if ('phase_gate_status' in data && data.phase_gate_status !== null && typeof data.phase_gate_status !== 'object') {
95
+ errors.push('phase_gate_status must be an object');
96
+ }
97
+ if ('budget_status' in data && data.budget_status !== null && typeof data.budget_status !== 'object') {
98
+ errors.push('budget_status must be an object');
99
+ }
100
+ if (isV1_1 && 'budget_reservations' in data && data.budget_reservations !== null && typeof data.budget_reservations !== 'object') {
101
+ errors.push('budget_reservations must be an object when provided');
102
+ }
103
+
104
+ if (data.status === 'blocked') {
105
+ if (typeof data.blocked_on !== 'string' || !data.blocked_on.trim()) {
106
+ errors.push('blocked_on must be a non-empty string when status is "blocked"');
107
+ }
108
+ if (!data.blocked_reason || typeof data.blocked_reason !== 'object') {
109
+ errors.push('blocked_reason must be an object when status is "blocked"');
110
+ } else {
111
+ if (typeof data.blocked_reason.category !== 'string' || !data.blocked_reason.category.trim()) {
112
+ errors.push('blocked_reason.category must be a non-empty string');
113
+ }
114
+ if (typeof data.blocked_reason.blocked_at !== 'string' || !data.blocked_reason.blocked_at.trim()) {
115
+ errors.push('blocked_reason.blocked_at must be a non-empty string');
116
+ }
117
+ if (!('turn_id' in data.blocked_reason) || (data.blocked_reason.turn_id !== null && typeof data.blocked_reason.turn_id !== 'string')) {
118
+ errors.push('blocked_reason.turn_id must be a string or null');
119
+ }
120
+ const recovery = data.blocked_reason.recovery;
121
+ if (!recovery || typeof recovery !== 'object') {
122
+ errors.push('blocked_reason.recovery must be an object');
123
+ } else {
124
+ if (typeof recovery.typed_reason !== 'string' || !recovery.typed_reason.trim()) errors.push('blocked_reason.recovery.typed_reason must be a non-empty string');
125
+ if (typeof recovery.owner !== 'string' || !recovery.owner.trim()) errors.push('blocked_reason.recovery.owner must be a non-empty string');
126
+ if (typeof recovery.recovery_action !== 'string' || !recovery.recovery_action.trim()) errors.push('blocked_reason.recovery.recovery_action must be a non-empty string');
127
+ if (typeof recovery.turn_retained !== 'boolean') errors.push('blocked_reason.recovery.turn_retained must be a boolean');
128
+ if ('detail' in recovery && recovery.detail !== null && typeof recovery.detail !== 'string') {
129
+ errors.push('blocked_reason.recovery.detail must be a string or null');
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ if (data.status === 'paused' && !data.pending_phase_transition && !data.pending_run_completion) {
136
+ errors.push('paused state must include pending_phase_transition or pending_run_completion');
137
+ }
138
+
139
+ if (data.status === 'active' && data.blocked_on != null) {
140
+ errors.push('active state must not include blocked_on');
141
+ }
142
+
143
+ return { ok: errors.length === 0, errors };
144
+ }
145
+
146
+ export function validateProjectStateSchema(data) {
147
+ if (data && typeof data === 'object' && ('run_id' in data || 'current_turn' in data || 'active_turns' in data || 'phase_gate_status' in data)) {
148
+ return validateGovernedStateSchema(data);
149
+ }
150
+ return validateStateSchema(data);
151
+ }
152
+
32
153
  export function validateConfigSchema(data) {
33
154
  const errors = [];
34
155
  if (data === null || typeof data !== 'object') {
@@ -0,0 +1,205 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://agentxchain.dev/schemas/turn-result/v1",
4
+ "title": "AgentXchain Turn Result v1",
5
+ "description": "Structured output produced by every agent turn. The orchestrator validates and accepts/rejects based on this contract.",
6
+ "type": "object",
7
+ "required": [
8
+ "schema_version",
9
+ "run_id",
10
+ "turn_id",
11
+ "role",
12
+ "runtime_id",
13
+ "status",
14
+ "summary",
15
+ "decisions",
16
+ "objections",
17
+ "files_changed",
18
+ "verification",
19
+ "artifact",
20
+ "proposed_next_role"
21
+ ],
22
+ "additionalProperties": false,
23
+ "properties": {
24
+ "schema_version": {
25
+ "const": "1.0",
26
+ "description": "Must be '1.0' for v1 turn results."
27
+ },
28
+ "run_id": {
29
+ "type": "string",
30
+ "minLength": 1,
31
+ "description": "Must match the current run. Format: run_XXXX..."
32
+ },
33
+ "turn_id": {
34
+ "type": "string",
35
+ "minLength": 1,
36
+ "description": "Must match the current turn assignment. Format: turn-NNNN"
37
+ },
38
+ "role": {
39
+ "type": "string",
40
+ "pattern": "^[a-z0-9_-]+$",
41
+ "description": "Must match the role assigned for this turn."
42
+ },
43
+ "runtime_id": {
44
+ "type": "string",
45
+ "minLength": 1,
46
+ "description": "The runtime that executed this turn."
47
+ },
48
+ "status": {
49
+ "enum": ["completed", "blocked", "needs_human", "failed"],
50
+ "description": "Outcome of this turn."
51
+ },
52
+ "summary": {
53
+ "type": "string",
54
+ "minLength": 1,
55
+ "description": "Human-readable summary of what happened."
56
+ },
57
+ "decisions": {
58
+ "type": "array",
59
+ "items": {
60
+ "type": "object",
61
+ "required": ["id", "category", "statement", "rationale"],
62
+ "additionalProperties": false,
63
+ "properties": {
64
+ "id": {
65
+ "type": "string",
66
+ "pattern": "^DEC-\\d+$",
67
+ "description": "Decision ID, e.g. DEC-014"
68
+ },
69
+ "category": {
70
+ "enum": ["implementation", "architecture", "scope", "process", "quality", "release"]
71
+ },
72
+ "statement": {
73
+ "type": "string",
74
+ "minLength": 1
75
+ },
76
+ "rationale": {
77
+ "type": "string",
78
+ "minLength": 1
79
+ }
80
+ }
81
+ }
82
+ },
83
+ "objections": {
84
+ "type": "array",
85
+ "items": {
86
+ "type": "object",
87
+ "required": ["id", "severity", "statement"],
88
+ "additionalProperties": false,
89
+ "properties": {
90
+ "id": {
91
+ "type": "string",
92
+ "pattern": "^OBJ-\\d+$",
93
+ "description": "Objection ID, e.g. OBJ-007"
94
+ },
95
+ "severity": {
96
+ "enum": ["low", "medium", "high", "blocking"]
97
+ },
98
+ "against_turn_id": {
99
+ "type": "string",
100
+ "description": "Turn that this objection challenges."
101
+ },
102
+ "against_decision_id": {
103
+ "type": "string",
104
+ "description": "Decision ID being challenged."
105
+ },
106
+ "statement": {
107
+ "type": "string",
108
+ "minLength": 1
109
+ },
110
+ "proposed_resolution": {
111
+ "type": "string"
112
+ },
113
+ "status": {
114
+ "enum": ["raised", "acknowledged", "resolved", "escalated", "resolved_by_human", "resolved_by_director"],
115
+ "default": "raised"
116
+ }
117
+ }
118
+ }
119
+ },
120
+ "files_changed": {
121
+ "type": "array",
122
+ "items": { "type": "string" },
123
+ "description": "Paths of files modified during this turn. Must match observed diff for authoritative runtimes."
124
+ },
125
+ "artifacts_created": {
126
+ "type": "array",
127
+ "items": { "type": "string" },
128
+ "description": "Paths of planning/review artifacts created (under .planning/ or .agentxchain/)."
129
+ },
130
+ "verification": {
131
+ "type": "object",
132
+ "required": ["status"],
133
+ "additionalProperties": false,
134
+ "properties": {
135
+ "status": {
136
+ "enum": ["pass", "fail", "skipped"],
137
+ "description": "Raw status from agent. Orchestrator may normalize to attested_pass or not_reproducible."
138
+ },
139
+ "commands": {
140
+ "type": "array",
141
+ "items": { "type": "string" },
142
+ "description": "Verification commands that were run."
143
+ },
144
+ "evidence_summary": {
145
+ "type": "string",
146
+ "description": "Human-readable description of verification evidence."
147
+ },
148
+ "machine_evidence": {
149
+ "type": "array",
150
+ "items": {
151
+ "type": "object",
152
+ "required": ["command", "exit_code"],
153
+ "properties": {
154
+ "command": { "type": "string" },
155
+ "exit_code": { "type": "integer" }
156
+ }
157
+ }
158
+ }
159
+ }
160
+ },
161
+ "artifact": {
162
+ "type": "object",
163
+ "required": ["type"],
164
+ "additionalProperties": false,
165
+ "properties": {
166
+ "type": {
167
+ "enum": ["workspace", "patch", "commit", "review"],
168
+ "description": "workspace = direct local changes, patch = unified diff, commit = git commit, review = no code changes"
169
+ },
170
+ "ref": {
171
+ "type": ["string", "null"],
172
+ "description": "Git SHA, patch path, or null for review-only."
173
+ },
174
+ "diff_summary": {
175
+ "type": "string"
176
+ }
177
+ }
178
+ },
179
+ "proposed_next_role": {
180
+ "type": "string",
181
+ "pattern": "^[a-z0-9_-]+$|^human$",
182
+ "description": "Role that should work next. Must be in routing allowlist or 'human'."
183
+ },
184
+ "phase_transition_request": {
185
+ "type": ["string", "null"],
186
+ "description": "If the agent believes the phase should advance, name the target phase."
187
+ },
188
+ "run_completion_request": {
189
+ "type": ["boolean", "null"],
190
+ "description": "Set to true only in the final phase when the run should complete. Mutually exclusive with phase_transition_request."
191
+ },
192
+ "needs_human_reason": {
193
+ "type": ["string", "null"],
194
+ "description": "If status is needs_human, explain why."
195
+ },
196
+ "cost": {
197
+ "type": "object",
198
+ "properties": {
199
+ "input_tokens": { "type": "integer", "minimum": 0 },
200
+ "output_tokens": { "type": "integer", "minimum": 0 },
201
+ "usd": { "type": "number", "minimum": 0 }
202
+ }
203
+ }
204
+ }
205
+ }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Token budget evaluator for preflight tokenization.
3
+ *
4
+ * Combines the context section parser, context compressor, and token counter
5
+ * to produce a TokenBudgetReport and effective context for api_proxy dispatch.
6
+ *
7
+ * This is a pure computation helper — it does not perform network dispatch,
8
+ * write audit artifacts, or mutate state.
9
+ */
10
+
11
+ import { countTokens } from './token-counter.js';
12
+ import { parseContextSections, renderContextSections } from './context-section-parser.js';
13
+ import { compressContextSections } from './context-compressor.js';
14
+ import { getDispatchEffectiveContextPath } from './turn-paths.js';
15
+
16
+ const SEPARATOR = '\n\n---\n\n';
17
+
18
+ const SYSTEM_PROMPT = [
19
+ 'You are acting as a governed agent in an AgentXchain protocol run.',
20
+ 'Your task and rules are described in the user message.',
21
+ 'You MUST respond with a valid JSON object matching the turn result schema provided in the prompt.',
22
+ 'Do NOT wrap the JSON in markdown code fences. Respond with raw JSON only.',
23
+ ].join('\n');
24
+
25
+ /**
26
+ * Evaluate the token budget for a preflight tokenization check.
27
+ *
28
+ * @param {object} params
29
+ * @param {string} params.promptMd - Full PROMPT.md text
30
+ * @param {string} params.contextMd - Full CONTEXT.md text (may be empty)
31
+ * @param {string} params.provider - Provider name (e.g. "anthropic")
32
+ * @param {string} params.model - Model identifier
33
+ * @param {string} params.runtimeId - Runtime identifier for the report
34
+ * @param {string} params.runId - Run identifier for the report
35
+ * @param {string} params.turnId - Turn identifier for the report
36
+ * @param {number} params.contextWindowTokens - Total context window size
37
+ * @param {number} params.maxOutputTokens - Reserved output tokens
38
+ * @param {number} params.safetyMarginTokens - Safety margin tokens
39
+ * @returns {TokenBudgetResult}
40
+ */
41
+ export function evaluateTokenBudget({
42
+ promptMd,
43
+ contextMd,
44
+ provider,
45
+ model,
46
+ runtimeId,
47
+ runId,
48
+ turnId,
49
+ contextWindowTokens,
50
+ maxOutputTokens,
51
+ safetyMarginTokens,
52
+ }) {
53
+ const reservedOutputTokens = maxOutputTokens || 4096;
54
+ const safetyMargin = safetyMarginTokens ?? 2048;
55
+ const availableInputTokens = contextWindowTokens - reservedOutputTokens - safetyMargin;
56
+
57
+ // Count immutable parts
58
+ const systemPromptTokens = countTokens(SYSTEM_PROMPT, provider);
59
+ const promptTokens = countTokens(promptMd || '', provider);
60
+ const hasSeparator = !!(contextMd && contextMd.trim());
61
+ const separatorTokens = hasSeparator ? countTokens(SEPARATOR, provider) : 0;
62
+ const immutableTokens = systemPromptTokens + promptTokens + separatorTokens;
63
+
64
+ // Build base report fields
65
+ const baseReport = {
66
+ provider,
67
+ model,
68
+ runtime_id: runtimeId,
69
+ run_id: runId,
70
+ turn_id: turnId,
71
+ tokenizer: 'provider_local',
72
+ context_window_tokens: contextWindowTokens,
73
+ reserved_output_tokens: reservedOutputTokens,
74
+ safety_margin_tokens: safetyMargin,
75
+ available_input_tokens: availableInputTokens,
76
+ system_prompt_tokens: systemPromptTokens,
77
+ prompt_tokens: promptTokens,
78
+ separator_tokens: separatorTokens,
79
+ };
80
+
81
+ // Prompt-only overflow: if immutable parts alone exceed budget, fail immediately
82
+ if (immutableTokens > availableInputTokens) {
83
+ const originalContextTokens = hasSeparator
84
+ ? countTokens(contextMd, provider)
85
+ : 0;
86
+
87
+ return {
88
+ sent_to_provider: false,
89
+ effective_context: hasSeparator ? contextMd : '',
90
+ report: {
91
+ ...baseReport,
92
+ original_context_tokens: originalContextTokens,
93
+ final_context_tokens: originalContextTokens,
94
+ estimated_input_tokens: immutableTokens + originalContextTokens,
95
+ truncated: false,
96
+ sent_to_provider: false,
97
+ effective_context_path: getDispatchEffectiveContextPath(turnId),
98
+ sections: [],
99
+ },
100
+ };
101
+ }
102
+
103
+ // No context case
104
+ if (!hasSeparator) {
105
+ return {
106
+ sent_to_provider: true,
107
+ effective_context: '',
108
+ report: {
109
+ ...baseReport,
110
+ original_context_tokens: 0,
111
+ final_context_tokens: 0,
112
+ estimated_input_tokens: immutableTokens,
113
+ truncated: false,
114
+ sent_to_provider: true,
115
+ effective_context_path: getDispatchEffectiveContextPath(turnId),
116
+ sections: [],
117
+ },
118
+ };
119
+ }
120
+
121
+ // Parse context into sections and count original tokens per section
122
+ const sections = parseContextSections(contextMd);
123
+ const originalTokensById = new Map();
124
+ for (const section of sections) {
125
+ originalTokensById.set(section.id, countTokens(section.content, provider));
126
+ }
127
+
128
+ const originalContextTokens = countTokens(contextMd, provider);
129
+
130
+ // Budget callback for the compressor: check if the full outbound request fits
131
+ function fitsInBudget(effectiveContext) {
132
+ const contextTokens = countTokens(effectiveContext, provider);
133
+ return immutableTokens + contextTokens <= availableInputTokens;
134
+ }
135
+
136
+ // Check if it fits without compression
137
+ if (fitsInBudget(contextMd)) {
138
+ const sectionActions = sections.map((s) => ({
139
+ id: s.id,
140
+ required: s.required,
141
+ original_tokens: originalTokensById.get(s.id),
142
+ final_tokens: originalTokensById.get(s.id),
143
+ action: 'kept',
144
+ }));
145
+
146
+ return {
147
+ sent_to_provider: true,
148
+ effective_context: contextMd,
149
+ report: {
150
+ ...baseReport,
151
+ original_context_tokens: originalContextTokens,
152
+ final_context_tokens: originalContextTokens,
153
+ estimated_input_tokens: immutableTokens + originalContextTokens,
154
+ truncated: false,
155
+ sent_to_provider: true,
156
+ effective_context_path: getDispatchEffectiveContextPath(turnId),
157
+ sections: sectionActions,
158
+ },
159
+ };
160
+ }
161
+
162
+ // Run compression
163
+ const compressionResult = compressContextSections(sections, fitsInBudget);
164
+ const effectiveContext = compressionResult.effective_context;
165
+ const finalContextTokens = countTokens(effectiveContext, provider);
166
+ const estimatedInputTokens = immutableTokens + finalContextTokens;
167
+ const sentToProvider = !compressionResult.exhausted;
168
+
169
+ // Build per-section actions with token counts
170
+ const sectionActions = compressionResult.actions.map((a) => {
171
+ let finalTokens;
172
+ if (a.action === 'dropped') {
173
+ finalTokens = 0;
174
+ } else if (a.action === 'truncated') {
175
+ const remaining = compressionResult.sections.find((s) => s.id === a.id);
176
+ finalTokens = remaining ? countTokens(remaining.content, provider) : 0;
177
+ } else {
178
+ finalTokens = originalTokensById.get(a.id) ?? 0;
179
+ }
180
+
181
+ return {
182
+ id: a.id,
183
+ required: a.required,
184
+ original_tokens: originalTokensById.get(a.id) ?? 0,
185
+ final_tokens: finalTokens,
186
+ action: a.action,
187
+ };
188
+ });
189
+
190
+ return {
191
+ sent_to_provider: sentToProvider,
192
+ effective_context: effectiveContext,
193
+ report: {
194
+ ...baseReport,
195
+ original_context_tokens: originalContextTokens,
196
+ final_context_tokens: finalContextTokens,
197
+ estimated_input_tokens: estimatedInputTokens,
198
+ truncated: compressionResult.steps_applied > 0,
199
+ sent_to_provider: sentToProvider,
200
+ effective_context_path: getDispatchEffectiveContextPath(turnId),
201
+ sections: sectionActions,
202
+ },
203
+ };
204
+ }
205
+
206
+ export { SYSTEM_PROMPT, SEPARATOR };
@@ -0,0 +1,27 @@
1
+ import { countTokens as countAnthropicTokens } from '@anthropic-ai/tokenizer';
2
+
3
+ const SUPPORTED_TOKEN_COUNTER_PROVIDERS = ['anthropic'];
4
+
5
+ export { SUPPORTED_TOKEN_COUNTER_PROVIDERS };
6
+
7
+ export function countTokens(text, provider = 'anthropic') {
8
+ const normalizedProvider = String(provider || '').trim().toLowerCase();
9
+
10
+ if (!SUPPORTED_TOKEN_COUNTER_PROVIDERS.includes(normalizedProvider)) {
11
+ throw new Error(
12
+ `Unsupported token counter provider "${provider}". Supported: ${SUPPORTED_TOKEN_COUNTER_PROVIDERS.join(', ')}`
13
+ );
14
+ }
15
+
16
+ const normalizedText = String(text ?? '');
17
+ if (!normalizedText) {
18
+ return 0;
19
+ }
20
+
21
+ const tokens = countAnthropicTokens(normalizedText);
22
+ if (!Number.isInteger(tokens) || tokens < 0) {
23
+ throw new Error(`Anthropic tokenizer returned invalid token count: ${tokens}`);
24
+ }
25
+
26
+ return tokens;
27
+ }
@@ -0,0 +1,67 @@
1
+ const DISPATCH_ROOT = '.agentxchain/dispatch';
2
+ const DISPATCH_INDEX_PATH = `${DISPATCH_ROOT}/index.json`;
3
+ const DISPATCH_TURNS_DIR = `${DISPATCH_ROOT}/turns`;
4
+ const STAGING_ROOT = '.agentxchain/staging';
5
+
6
+ export function getDispatchTurnDir(turnId) {
7
+ return `${DISPATCH_TURNS_DIR}/${turnId}`;
8
+ }
9
+
10
+ export function getDispatchPromptPath(turnId) {
11
+ return `${getDispatchTurnDir(turnId)}/PROMPT.md`;
12
+ }
13
+
14
+ export function getDispatchContextPath(turnId) {
15
+ return `${getDispatchTurnDir(turnId)}/CONTEXT.md`;
16
+ }
17
+
18
+ export function getDispatchAssignmentPath(turnId) {
19
+ return `${getDispatchTurnDir(turnId)}/ASSIGNMENT.json`;
20
+ }
21
+
22
+ export function getDispatchApiRequestPath(turnId) {
23
+ return `${getDispatchTurnDir(turnId)}/API_REQUEST.json`;
24
+ }
25
+
26
+ export function getDispatchTokenBudgetPath(turnId) {
27
+ return `${getDispatchTurnDir(turnId)}/TOKEN_BUDGET.json`;
28
+ }
29
+
30
+ export function getDispatchEffectiveContextPath(turnId) {
31
+ return `${getDispatchTurnDir(turnId)}/CONTEXT.effective.md`;
32
+ }
33
+
34
+ export function getDispatchManifestPath(turnId) {
35
+ return `${getDispatchTurnDir(turnId)}/MANIFEST.json`;
36
+ }
37
+
38
+ export function getDispatchLogPath(turnId) {
39
+ return `${getDispatchTurnDir(turnId)}/stdout.log`;
40
+ }
41
+
42
+ export function getTurnStagingDir(turnId) {
43
+ return `${STAGING_ROOT}/${turnId}`;
44
+ }
45
+
46
+ export function getTurnStagingResultPath(turnId) {
47
+ return `${getTurnStagingDir(turnId)}/turn-result.json`;
48
+ }
49
+
50
+ export function getTurnProviderResponsePath(turnId) {
51
+ return `${getTurnStagingDir(turnId)}/provider-response.json`;
52
+ }
53
+
54
+ export function getTurnApiErrorPath(turnId) {
55
+ return `${getTurnStagingDir(turnId)}/api-error.json`;
56
+ }
57
+
58
+ export function getTurnRetryTracePath(turnId) {
59
+ return `${getTurnStagingDir(turnId)}/retry-trace.json`;
60
+ }
61
+
62
+ export {
63
+ DISPATCH_ROOT,
64
+ DISPATCH_INDEX_PATH,
65
+ DISPATCH_TURNS_DIR,
66
+ STAGING_ROOT,
67
+ };