@renseiai/agentfactory 0.8.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/LICENSE +21 -0
- package/README.md +125 -0
- package/dist/src/config/index.d.ts +3 -0
- package/dist/src/config/index.d.ts.map +1 -0
- package/dist/src/config/index.js +1 -0
- package/dist/src/config/repository-config.d.ts +44 -0
- package/dist/src/config/repository-config.d.ts.map +1 -0
- package/dist/src/config/repository-config.js +88 -0
- package/dist/src/config/repository-config.test.d.ts +2 -0
- package/dist/src/config/repository-config.test.d.ts.map +1 -0
- package/dist/src/config/repository-config.test.js +249 -0
- package/dist/src/deployment/deployment-checker.d.ts +110 -0
- package/dist/src/deployment/deployment-checker.d.ts.map +1 -0
- package/dist/src/deployment/deployment-checker.js +242 -0
- package/dist/src/deployment/index.d.ts +3 -0
- package/dist/src/deployment/index.d.ts.map +1 -0
- package/dist/src/deployment/index.js +2 -0
- package/dist/src/frontend/index.d.ts +2 -0
- package/dist/src/frontend/index.d.ts.map +1 -0
- package/dist/src/frontend/index.js +1 -0
- package/dist/src/frontend/types.d.ts +106 -0
- package/dist/src/frontend/types.d.ts.map +1 -0
- package/dist/src/frontend/types.js +11 -0
- package/dist/src/governor/decision-engine.d.ts +52 -0
- package/dist/src/governor/decision-engine.d.ts.map +1 -0
- package/dist/src/governor/decision-engine.js +220 -0
- package/dist/src/governor/decision-engine.test.d.ts +2 -0
- package/dist/src/governor/decision-engine.test.d.ts.map +1 -0
- package/dist/src/governor/decision-engine.test.js +629 -0
- package/dist/src/governor/event-bus.d.ts +43 -0
- package/dist/src/governor/event-bus.d.ts.map +1 -0
- package/dist/src/governor/event-bus.js +8 -0
- package/dist/src/governor/event-deduplicator.d.ts +43 -0
- package/dist/src/governor/event-deduplicator.d.ts.map +1 -0
- package/dist/src/governor/event-deduplicator.js +53 -0
- package/dist/src/governor/event-driven-governor.d.ts +131 -0
- package/dist/src/governor/event-driven-governor.d.ts.map +1 -0
- package/dist/src/governor/event-driven-governor.js +379 -0
- package/dist/src/governor/event-driven-governor.test.d.ts +2 -0
- package/dist/src/governor/event-driven-governor.test.d.ts.map +1 -0
- package/dist/src/governor/event-driven-governor.test.js +673 -0
- package/dist/src/governor/event-types.d.ts +78 -0
- package/dist/src/governor/event-types.d.ts.map +1 -0
- package/dist/src/governor/event-types.js +32 -0
- package/dist/src/governor/governor-types.d.ts +82 -0
- package/dist/src/governor/governor-types.d.ts.map +1 -0
- package/dist/src/governor/governor-types.js +21 -0
- package/dist/src/governor/governor.d.ts +100 -0
- package/dist/src/governor/governor.d.ts.map +1 -0
- package/dist/src/governor/governor.js +262 -0
- package/dist/src/governor/governor.test.d.ts +2 -0
- package/dist/src/governor/governor.test.d.ts.map +1 -0
- package/dist/src/governor/governor.test.js +514 -0
- package/dist/src/governor/human-touchpoints.d.ts +131 -0
- package/dist/src/governor/human-touchpoints.d.ts.map +1 -0
- package/dist/src/governor/human-touchpoints.js +251 -0
- package/dist/src/governor/human-touchpoints.test.d.ts +2 -0
- package/dist/src/governor/human-touchpoints.test.d.ts.map +1 -0
- package/dist/src/governor/human-touchpoints.test.js +366 -0
- package/dist/src/governor/in-memory-event-bus.d.ts +29 -0
- package/dist/src/governor/in-memory-event-bus.d.ts.map +1 -0
- package/dist/src/governor/in-memory-event-bus.js +79 -0
- package/dist/src/governor/index.d.ts +14 -0
- package/dist/src/governor/index.d.ts.map +1 -0
- package/dist/src/governor/index.js +13 -0
- package/dist/src/governor/override-parser.d.ts +60 -0
- package/dist/src/governor/override-parser.d.ts.map +1 -0
- package/dist/src/governor/override-parser.js +98 -0
- package/dist/src/governor/override-parser.test.d.ts +2 -0
- package/dist/src/governor/override-parser.test.d.ts.map +1 -0
- package/dist/src/governor/override-parser.test.js +312 -0
- package/dist/src/governor/platform-adapter.d.ts +69 -0
- package/dist/src/governor/platform-adapter.d.ts.map +1 -0
- package/dist/src/governor/platform-adapter.js +11 -0
- package/dist/src/governor/processing-state.d.ts +66 -0
- package/dist/src/governor/processing-state.d.ts.map +1 -0
- package/dist/src/governor/processing-state.js +43 -0
- package/dist/src/governor/processing-state.test.d.ts +2 -0
- package/dist/src/governor/processing-state.test.d.ts.map +1 -0
- package/dist/src/governor/processing-state.test.js +96 -0
- package/dist/src/governor/top-of-funnel.d.ts +118 -0
- package/dist/src/governor/top-of-funnel.d.ts.map +1 -0
- package/dist/src/governor/top-of-funnel.js +168 -0
- package/dist/src/governor/top-of-funnel.test.d.ts +2 -0
- package/dist/src/governor/top-of-funnel.test.d.ts.map +1 -0
- package/dist/src/governor/top-of-funnel.test.js +331 -0
- package/dist/src/index.d.ts +11 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +10 -0
- package/dist/src/linear-cli.d.ts +38 -0
- package/dist/src/linear-cli.d.ts.map +1 -0
- package/dist/src/linear-cli.js +674 -0
- package/dist/src/logger.d.ts +117 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +430 -0
- package/dist/src/manifest/generate.d.ts +20 -0
- package/dist/src/manifest/generate.d.ts.map +1 -0
- package/dist/src/manifest/generate.js +65 -0
- package/dist/src/manifest/index.d.ts +4 -0
- package/dist/src/manifest/index.d.ts.map +1 -0
- package/dist/src/manifest/index.js +2 -0
- package/dist/src/manifest/route-manifest.d.ts +34 -0
- package/dist/src/manifest/route-manifest.d.ts.map +1 -0
- package/dist/src/manifest/route-manifest.js +148 -0
- package/dist/src/orchestrator/activity-emitter.d.ts +119 -0
- package/dist/src/orchestrator/activity-emitter.d.ts.map +1 -0
- package/dist/src/orchestrator/activity-emitter.js +306 -0
- package/dist/src/orchestrator/api-activity-emitter.d.ts +167 -0
- package/dist/src/orchestrator/api-activity-emitter.d.ts.map +1 -0
- package/dist/src/orchestrator/api-activity-emitter.js +417 -0
- package/dist/src/orchestrator/heartbeat-writer.d.ts +57 -0
- package/dist/src/orchestrator/heartbeat-writer.d.ts.map +1 -0
- package/dist/src/orchestrator/heartbeat-writer.js +137 -0
- package/dist/src/orchestrator/index.d.ts +20 -0
- package/dist/src/orchestrator/index.d.ts.map +1 -0
- package/dist/src/orchestrator/index.js +22 -0
- package/dist/src/orchestrator/log-analyzer.d.ts +160 -0
- package/dist/src/orchestrator/log-analyzer.d.ts.map +1 -0
- package/dist/src/orchestrator/log-analyzer.js +572 -0
- package/dist/src/orchestrator/log-config.d.ts +39 -0
- package/dist/src/orchestrator/log-config.d.ts.map +1 -0
- package/dist/src/orchestrator/log-config.js +45 -0
- package/dist/src/orchestrator/orchestrator.d.ts +316 -0
- package/dist/src/orchestrator/orchestrator.d.ts.map +1 -0
- package/dist/src/orchestrator/orchestrator.js +3290 -0
- package/dist/src/orchestrator/parse-work-result.d.ts +16 -0
- package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -0
- package/dist/src/orchestrator/parse-work-result.js +135 -0
- package/dist/src/orchestrator/parse-work-result.test.d.ts +2 -0
- package/dist/src/orchestrator/parse-work-result.test.d.ts.map +1 -0
- package/dist/src/orchestrator/parse-work-result.test.js +234 -0
- package/dist/src/orchestrator/progress-logger.d.ts +72 -0
- package/dist/src/orchestrator/progress-logger.d.ts.map +1 -0
- package/dist/src/orchestrator/progress-logger.js +135 -0
- package/dist/src/orchestrator/session-logger.d.ts +159 -0
- package/dist/src/orchestrator/session-logger.d.ts.map +1 -0
- package/dist/src/orchestrator/session-logger.js +275 -0
- package/dist/src/orchestrator/state-recovery.d.ts +96 -0
- package/dist/src/orchestrator/state-recovery.d.ts.map +1 -0
- package/dist/src/orchestrator/state-recovery.js +302 -0
- package/dist/src/orchestrator/state-types.d.ts +165 -0
- package/dist/src/orchestrator/state-types.d.ts.map +1 -0
- package/dist/src/orchestrator/state-types.js +7 -0
- package/dist/src/orchestrator/stream-parser.d.ts +151 -0
- package/dist/src/orchestrator/stream-parser.d.ts.map +1 -0
- package/dist/src/orchestrator/stream-parser.js +137 -0
- package/dist/src/orchestrator/types.d.ts +232 -0
- package/dist/src/orchestrator/types.d.ts.map +1 -0
- package/dist/src/orchestrator/types.js +4 -0
- package/dist/src/orchestrator/validate-git-remote.test.d.ts +2 -0
- package/dist/src/orchestrator/validate-git-remote.test.d.ts.map +1 -0
- package/dist/src/orchestrator/validate-git-remote.test.js +61 -0
- package/dist/src/providers/a2a-auth.d.ts +81 -0
- package/dist/src/providers/a2a-auth.d.ts.map +1 -0
- package/dist/src/providers/a2a-auth.js +188 -0
- package/dist/src/providers/a2a-auth.test.d.ts +2 -0
- package/dist/src/providers/a2a-auth.test.d.ts.map +1 -0
- package/dist/src/providers/a2a-auth.test.js +232 -0
- package/dist/src/providers/a2a-provider.d.ts +254 -0
- package/dist/src/providers/a2a-provider.d.ts.map +1 -0
- package/dist/src/providers/a2a-provider.integration.test.d.ts +9 -0
- package/dist/src/providers/a2a-provider.integration.test.d.ts.map +1 -0
- package/dist/src/providers/a2a-provider.integration.test.js +665 -0
- package/dist/src/providers/a2a-provider.js +811 -0
- package/dist/src/providers/a2a-provider.test.d.ts +2 -0
- package/dist/src/providers/a2a-provider.test.d.ts.map +1 -0
- package/dist/src/providers/a2a-provider.test.js +681 -0
- package/dist/src/providers/amp-provider.d.ts +20 -0
- package/dist/src/providers/amp-provider.d.ts.map +1 -0
- package/dist/src/providers/amp-provider.js +24 -0
- package/dist/src/providers/claude-provider.d.ts +18 -0
- package/dist/src/providers/claude-provider.d.ts.map +1 -0
- package/dist/src/providers/claude-provider.js +437 -0
- package/dist/src/providers/codex-provider.d.ts +133 -0
- package/dist/src/providers/codex-provider.d.ts.map +1 -0
- package/dist/src/providers/codex-provider.js +381 -0
- package/dist/src/providers/codex-provider.test.d.ts +2 -0
- package/dist/src/providers/codex-provider.test.d.ts.map +1 -0
- package/dist/src/providers/codex-provider.test.js +387 -0
- package/dist/src/providers/index.d.ts +44 -0
- package/dist/src/providers/index.d.ts.map +1 -0
- package/dist/src/providers/index.js +85 -0
- package/dist/src/providers/spring-ai-provider.d.ts +90 -0
- package/dist/src/providers/spring-ai-provider.d.ts.map +1 -0
- package/dist/src/providers/spring-ai-provider.integration.test.d.ts +13 -0
- package/dist/src/providers/spring-ai-provider.integration.test.d.ts.map +1 -0
- package/dist/src/providers/spring-ai-provider.integration.test.js +351 -0
- package/dist/src/providers/spring-ai-provider.js +317 -0
- package/dist/src/providers/spring-ai-provider.test.d.ts +2 -0
- package/dist/src/providers/spring-ai-provider.test.d.ts.map +1 -0
- package/dist/src/providers/spring-ai-provider.test.js +200 -0
- package/dist/src/providers/types.d.ts +165 -0
- package/dist/src/providers/types.d.ts.map +1 -0
- package/dist/src/providers/types.js +13 -0
- package/dist/src/templates/adapters.d.ts +51 -0
- package/dist/src/templates/adapters.d.ts.map +1 -0
- package/dist/src/templates/adapters.js +104 -0
- package/dist/src/templates/adapters.test.d.ts +2 -0
- package/dist/src/templates/adapters.test.d.ts.map +1 -0
- package/dist/src/templates/adapters.test.js +165 -0
- package/dist/src/templates/agent-definition.d.ts +85 -0
- package/dist/src/templates/agent-definition.d.ts.map +1 -0
- package/dist/src/templates/agent-definition.js +97 -0
- package/dist/src/templates/agent-definition.test.d.ts +2 -0
- package/dist/src/templates/agent-definition.test.d.ts.map +1 -0
- package/dist/src/templates/agent-definition.test.js +209 -0
- package/dist/src/templates/index.d.ts +14 -0
- package/dist/src/templates/index.d.ts.map +1 -0
- package/dist/src/templates/index.js +11 -0
- package/dist/src/templates/loader.d.ts +41 -0
- package/dist/src/templates/loader.d.ts.map +1 -0
- package/dist/src/templates/loader.js +114 -0
- package/dist/src/templates/registry.d.ts +80 -0
- package/dist/src/templates/registry.d.ts.map +1 -0
- package/dist/src/templates/registry.js +177 -0
- package/dist/src/templates/registry.test.d.ts +2 -0
- package/dist/src/templates/registry.test.d.ts.map +1 -0
- package/dist/src/templates/registry.test.js +198 -0
- package/dist/src/templates/renderer.d.ts +29 -0
- package/dist/src/templates/renderer.d.ts.map +1 -0
- package/dist/src/templates/renderer.js +35 -0
- package/dist/src/templates/strategy-templates.test.d.ts +2 -0
- package/dist/src/templates/strategy-templates.test.d.ts.map +1 -0
- package/dist/src/templates/strategy-templates.test.js +619 -0
- package/dist/src/templates/types.d.ts +233 -0
- package/dist/src/templates/types.d.ts.map +1 -0
- package/dist/src/templates/types.js +127 -0
- package/dist/src/templates/types.test.d.ts +2 -0
- package/dist/src/templates/types.test.d.ts.map +1 -0
- package/dist/src/templates/types.test.js +232 -0
- package/dist/src/tools/index.d.ts +6 -0
- package/dist/src/tools/index.d.ts.map +1 -0
- package/dist/src/tools/index.js +3 -0
- package/dist/src/tools/linear-runner.d.ts +34 -0
- package/dist/src/tools/linear-runner.d.ts.map +1 -0
- package/dist/src/tools/linear-runner.js +700 -0
- package/dist/src/tools/plugins/linear.d.ts +9 -0
- package/dist/src/tools/plugins/linear.d.ts.map +1 -0
- package/dist/src/tools/plugins/linear.js +138 -0
- package/dist/src/tools/registry.d.ts +9 -0
- package/dist/src/tools/registry.d.ts.map +1 -0
- package/dist/src/tools/registry.js +18 -0
- package/dist/src/tools/types.d.ts +18 -0
- package/dist/src/tools/types.d.ts.map +1 -0
- package/dist/src/tools/types.js +1 -0
- package/package.json +78 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for Spring AI Provider
|
|
3
|
+
*
|
|
4
|
+
* Tests the full provider lifecycle using mock subprocess that emits
|
|
5
|
+
* Spring AI JSONL events, covering:
|
|
6
|
+
* - Happy path spawn + complete
|
|
7
|
+
* - Session resume
|
|
8
|
+
* - Error handling (missing JAR, missing Java, agent crash)
|
|
9
|
+
* - Cost data extraction accuracy
|
|
10
|
+
* - Tool permission adapter output
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
13
|
+
import { SpringAiProvider } from './spring-ai-provider.js';
|
|
14
|
+
import { SpringAiToolPermissionAdapter, createToolPermissionAdapter } from '../templates/adapters.js';
|
|
15
|
+
import { PassThrough } from 'stream';
|
|
16
|
+
import { EventEmitter } from 'events';
|
|
17
|
+
function createMockChild(pid = 1234) {
|
|
18
|
+
const child = new EventEmitter();
|
|
19
|
+
child.pid = pid;
|
|
20
|
+
child.stdin = new PassThrough();
|
|
21
|
+
child.stdout = new PassThrough();
|
|
22
|
+
child.stderr = new PassThrough();
|
|
23
|
+
child.exitCode = null;
|
|
24
|
+
child.kill = vi.fn();
|
|
25
|
+
return child;
|
|
26
|
+
}
|
|
27
|
+
let mockChild;
|
|
28
|
+
vi.mock('child_process', () => ({
|
|
29
|
+
spawn: vi.fn(() => mockChild),
|
|
30
|
+
}));
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Helpers
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
function makeConfig(overrides) {
|
|
35
|
+
return {
|
|
36
|
+
prompt: 'Test prompt',
|
|
37
|
+
cwd: '/tmp/test',
|
|
38
|
+
env: { SPRING_AI_AGENT_JAR: '/path/to/agent.jar' },
|
|
39
|
+
abortController: new AbortController(),
|
|
40
|
+
autonomous: true,
|
|
41
|
+
sandboxEnabled: false,
|
|
42
|
+
...overrides,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function writeLine(child, data) {
|
|
46
|
+
child.stdout.write(JSON.stringify(data) + '\n');
|
|
47
|
+
}
|
|
48
|
+
function endProcess(child, exitCode = 0) {
|
|
49
|
+
child.stdout.end();
|
|
50
|
+
child.exitCode = exitCode;
|
|
51
|
+
child.emit('exit', exitCode);
|
|
52
|
+
}
|
|
53
|
+
async function collectEvents(stream) {
|
|
54
|
+
const events = [];
|
|
55
|
+
for await (const event of stream) {
|
|
56
|
+
events.push(event);
|
|
57
|
+
}
|
|
58
|
+
return events;
|
|
59
|
+
}
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Tests
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
describe('SpringAiProvider integration', () => {
|
|
64
|
+
let provider;
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
mockChild = createMockChild();
|
|
67
|
+
provider = new SpringAiProvider();
|
|
68
|
+
});
|
|
69
|
+
afterEach(() => {
|
|
70
|
+
vi.clearAllMocks();
|
|
71
|
+
});
|
|
72
|
+
describe('happy path spawn + complete', () => {
|
|
73
|
+
it('produces init, assistant_text, and result events', async () => {
|
|
74
|
+
const handle = provider.spawn(makeConfig());
|
|
75
|
+
expect(handle.sessionId).toBeNull();
|
|
76
|
+
// Emit events asynchronously
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
writeLine(mockChild, { type: 'session.started', session_id: 'sess-001' });
|
|
79
|
+
writeLine(mockChild, { type: 'turn.started' });
|
|
80
|
+
writeLine(mockChild, { type: 'assistant.message', id: 'msg-1', text: 'Hello world' });
|
|
81
|
+
writeLine(mockChild, {
|
|
82
|
+
type: 'turn.completed',
|
|
83
|
+
usage: { input_tokens: 150, output_tokens: 75, model: 'gpt-4' },
|
|
84
|
+
});
|
|
85
|
+
endProcess(mockChild, 0);
|
|
86
|
+
}, 10);
|
|
87
|
+
const events = await collectEvents(handle.stream);
|
|
88
|
+
expect(events).toHaveLength(4);
|
|
89
|
+
expect(events[0]).toMatchObject({ type: 'init', sessionId: 'sess-001' });
|
|
90
|
+
expect(events[1]).toMatchObject({ type: 'system', subtype: 'turn_started', message: 'Turn 1 started' });
|
|
91
|
+
expect(events[2]).toMatchObject({ type: 'assistant_text', text: 'Hello world' });
|
|
92
|
+
expect(events[3]).toMatchObject({
|
|
93
|
+
type: 'result',
|
|
94
|
+
success: true,
|
|
95
|
+
cost: { inputTokens: 150, outputTokens: 75, numTurns: 1 },
|
|
96
|
+
});
|
|
97
|
+
// sessionId should be set after init event
|
|
98
|
+
expect(handle.sessionId).toBe('sess-001');
|
|
99
|
+
});
|
|
100
|
+
it('handles multi-turn sessions with accumulated cost', async () => {
|
|
101
|
+
const handle = provider.spawn(makeConfig());
|
|
102
|
+
setTimeout(() => {
|
|
103
|
+
writeLine(mockChild, { type: 'session.started', session_id: 'sess-002' });
|
|
104
|
+
writeLine(mockChild, { type: 'turn.started' });
|
|
105
|
+
writeLine(mockChild, { type: 'assistant.message', id: 'msg-1', text: 'Analyzing...' });
|
|
106
|
+
writeLine(mockChild, {
|
|
107
|
+
type: 'turn.completed',
|
|
108
|
+
usage: { input_tokens: 100, output_tokens: 50 },
|
|
109
|
+
});
|
|
110
|
+
writeLine(mockChild, { type: 'turn.started' });
|
|
111
|
+
writeLine(mockChild, { type: 'assistant.message', id: 'msg-2', text: 'Done!' });
|
|
112
|
+
writeLine(mockChild, {
|
|
113
|
+
type: 'turn.completed',
|
|
114
|
+
usage: { input_tokens: 200, output_tokens: 80 },
|
|
115
|
+
});
|
|
116
|
+
endProcess(mockChild, 0);
|
|
117
|
+
}, 10);
|
|
118
|
+
const events = await collectEvents(handle.stream);
|
|
119
|
+
const resultEvents = events.filter(e => e.type === 'result');
|
|
120
|
+
expect(resultEvents).toHaveLength(2);
|
|
121
|
+
// Second result should have accumulated totals
|
|
122
|
+
expect(resultEvents[1]).toMatchObject({
|
|
123
|
+
type: 'result',
|
|
124
|
+
success: true,
|
|
125
|
+
cost: { inputTokens: 300, outputTokens: 130, numTurns: 2 },
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
describe('tool use events', () => {
|
|
130
|
+
it('maps tool invocation and result events', async () => {
|
|
131
|
+
const handle = provider.spawn(makeConfig());
|
|
132
|
+
setTimeout(() => {
|
|
133
|
+
writeLine(mockChild, { type: 'session.started', session_id: 'sess-003' });
|
|
134
|
+
writeLine(mockChild, { type: 'turn.started' });
|
|
135
|
+
writeLine(mockChild, {
|
|
136
|
+
type: 'tool.invocation',
|
|
137
|
+
id: 'tool-1',
|
|
138
|
+
tool_name: 'shell',
|
|
139
|
+
input: { command: 'ls -la' },
|
|
140
|
+
});
|
|
141
|
+
writeLine(mockChild, {
|
|
142
|
+
type: 'tool.result',
|
|
143
|
+
id: 'tool-1',
|
|
144
|
+
tool_name: 'shell',
|
|
145
|
+
content: 'file1.txt\nfile2.txt',
|
|
146
|
+
is_error: false,
|
|
147
|
+
});
|
|
148
|
+
writeLine(mockChild, { type: 'turn.completed' });
|
|
149
|
+
endProcess(mockChild, 0);
|
|
150
|
+
}, 10);
|
|
151
|
+
const events = await collectEvents(handle.stream);
|
|
152
|
+
const toolUse = events.find(e => e.type === 'tool_use');
|
|
153
|
+
const toolResult = events.find(e => e.type === 'tool_result');
|
|
154
|
+
expect(toolUse).toMatchObject({
|
|
155
|
+
type: 'tool_use',
|
|
156
|
+
toolName: 'shell',
|
|
157
|
+
toolUseId: 'tool-1',
|
|
158
|
+
input: { command: 'ls -la' },
|
|
159
|
+
});
|
|
160
|
+
expect(toolResult).toMatchObject({
|
|
161
|
+
type: 'tool_result',
|
|
162
|
+
toolName: 'shell',
|
|
163
|
+
toolUseId: 'tool-1',
|
|
164
|
+
content: 'file1.txt\nfile2.txt',
|
|
165
|
+
isError: false,
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
describe('session resume', () => {
|
|
170
|
+
it('passes resume session ID to CLI args', async () => {
|
|
171
|
+
const { spawn: mockSpawn } = await import('child_process');
|
|
172
|
+
const handle = provider.resume('sess-previous', makeConfig());
|
|
173
|
+
// Verify spawn was called with --resume flag
|
|
174
|
+
expect(mockSpawn).toHaveBeenCalled();
|
|
175
|
+
const callArgs = mockSpawn.mock.calls[0];
|
|
176
|
+
const args = callArgs[1];
|
|
177
|
+
expect(args).toContain('--resume');
|
|
178
|
+
expect(args).toContain('sess-previous');
|
|
179
|
+
// Complete the process to avoid hanging
|
|
180
|
+
setTimeout(() => {
|
|
181
|
+
writeLine(mockChild, { type: 'session.started', session_id: 'sess-previous' });
|
|
182
|
+
writeLine(mockChild, { type: 'turn.completed' });
|
|
183
|
+
endProcess(mockChild, 0);
|
|
184
|
+
}, 10);
|
|
185
|
+
const events = await collectEvents(handle.stream);
|
|
186
|
+
expect(events[0]).toMatchObject({ type: 'init', sessionId: 'sess-previous' });
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
describe('error handling', () => {
|
|
190
|
+
it('handles missing SPRING_AI_AGENT_JAR gracefully', async () => {
|
|
191
|
+
const config = makeConfig({ env: {} }); // No JAR path
|
|
192
|
+
const handle = provider.spawn(config);
|
|
193
|
+
const events = await collectEvents(handle.stream);
|
|
194
|
+
expect(events).toHaveLength(2);
|
|
195
|
+
expect(events[0]).toMatchObject({
|
|
196
|
+
type: 'error',
|
|
197
|
+
message: 'SPRING_AI_AGENT_JAR environment variable is not set',
|
|
198
|
+
});
|
|
199
|
+
expect(events[1]).toMatchObject({
|
|
200
|
+
type: 'result',
|
|
201
|
+
success: false,
|
|
202
|
+
errorSubtype: 'configuration_error',
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
it('handles agent crash (non-zero exit)', async () => {
|
|
206
|
+
const handle = provider.spawn(makeConfig());
|
|
207
|
+
setTimeout(() => {
|
|
208
|
+
writeLine(mockChild, { type: 'session.started', session_id: 'sess-crash' });
|
|
209
|
+
mockChild.stderr.write('Exception in thread "main" java.lang.OutOfMemoryError\n');
|
|
210
|
+
endProcess(mockChild, 1);
|
|
211
|
+
}, 10);
|
|
212
|
+
const events = await collectEvents(handle.stream);
|
|
213
|
+
const resultEvent = events.find(e => e.type === 'result');
|
|
214
|
+
expect(resultEvent).toMatchObject({
|
|
215
|
+
type: 'result',
|
|
216
|
+
success: false,
|
|
217
|
+
errorSubtype: 'process_exit',
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
it('detects missing Java runtime', async () => {
|
|
221
|
+
const handle = provider.spawn(makeConfig());
|
|
222
|
+
setTimeout(() => {
|
|
223
|
+
mockChild.stderr.write('java: not found\n');
|
|
224
|
+
endProcess(mockChild, 127);
|
|
225
|
+
}, 10);
|
|
226
|
+
const events = await collectEvents(handle.stream);
|
|
227
|
+
const resultEvent = events.find(e => e.type === 'result');
|
|
228
|
+
expect(resultEvent).toMatchObject({
|
|
229
|
+
type: 'result',
|
|
230
|
+
success: false,
|
|
231
|
+
errorSubtype: 'java_not_found',
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
it('detects missing JAR file', async () => {
|
|
235
|
+
const handle = provider.spawn(makeConfig());
|
|
236
|
+
setTimeout(() => {
|
|
237
|
+
mockChild.stderr.write('Error: Unable to access jarfile /path/to/agent.jar\n');
|
|
238
|
+
endProcess(mockChild, 1);
|
|
239
|
+
}, 10);
|
|
240
|
+
const events = await collectEvents(handle.stream);
|
|
241
|
+
const resultEvent = events.find(e => e.type === 'result');
|
|
242
|
+
expect(resultEvent).toMatchObject({
|
|
243
|
+
type: 'result',
|
|
244
|
+
success: false,
|
|
245
|
+
errorSubtype: 'jar_not_found',
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
it('maps error events from the agent', async () => {
|
|
249
|
+
const handle = provider.spawn(makeConfig());
|
|
250
|
+
setTimeout(() => {
|
|
251
|
+
writeLine(mockChild, { type: 'session.started', session_id: 'sess-err' });
|
|
252
|
+
writeLine(mockChild, { type: 'error', message: 'Rate limit exceeded', code: 'RATE_LIMIT' });
|
|
253
|
+
writeLine(mockChild, {
|
|
254
|
+
type: 'turn.failed',
|
|
255
|
+
error: { message: 'Rate limit exceeded' },
|
|
256
|
+
});
|
|
257
|
+
endProcess(mockChild, 1);
|
|
258
|
+
}, 10);
|
|
259
|
+
const events = await collectEvents(handle.stream);
|
|
260
|
+
const errorEvent = events.find(e => e.type === 'error');
|
|
261
|
+
const resultEvent = events.find(e => e.type === 'result');
|
|
262
|
+
expect(errorEvent).toMatchObject({
|
|
263
|
+
type: 'error',
|
|
264
|
+
message: 'Rate limit exceeded',
|
|
265
|
+
code: 'RATE_LIMIT',
|
|
266
|
+
});
|
|
267
|
+
expect(resultEvent).toMatchObject({
|
|
268
|
+
type: 'result',
|
|
269
|
+
success: false,
|
|
270
|
+
errors: ['Rate limit exceeded'],
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
describe('abort / stop', () => {
|
|
275
|
+
it('kills process on stop()', async () => {
|
|
276
|
+
const handle = provider.spawn(makeConfig());
|
|
277
|
+
// Start the stream but don't consume it yet
|
|
278
|
+
setTimeout(async () => {
|
|
279
|
+
writeLine(mockChild, { type: 'session.started', session_id: 'sess-stop' });
|
|
280
|
+
await handle.stop();
|
|
281
|
+
endProcess(mockChild, 0);
|
|
282
|
+
}, 10);
|
|
283
|
+
const events = await collectEvents(handle.stream);
|
|
284
|
+
expect(events.length).toBeGreaterThanOrEqual(1);
|
|
285
|
+
});
|
|
286
|
+
it('injectMessage throws (not supported)', async () => {
|
|
287
|
+
const handle = provider.spawn(makeConfig());
|
|
288
|
+
await expect(handle.injectMessage('test')).rejects.toThrow('Spring AI provider does not support mid-session message injection');
|
|
289
|
+
// Clean up process
|
|
290
|
+
setTimeout(() => endProcess(mockChild, 0), 10);
|
|
291
|
+
await collectEvents(handle.stream);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
describe('process spawned callback', () => {
|
|
295
|
+
it('calls onProcessSpawned with PID', () => {
|
|
296
|
+
const onProcessSpawned = vi.fn();
|
|
297
|
+
provider.spawn(makeConfig({ onProcessSpawned }));
|
|
298
|
+
expect(onProcessSpawned).toHaveBeenCalledWith(1234);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
describe('non-JSON output handling', () => {
|
|
302
|
+
it('emits system events for non-JSON lines', async () => {
|
|
303
|
+
const handle = provider.spawn(makeConfig());
|
|
304
|
+
setTimeout(() => {
|
|
305
|
+
mockChild.stdout.write('Spring AI Agent v0.1.0 starting...\n');
|
|
306
|
+
writeLine(mockChild, { type: 'session.started', session_id: 'sess-log' });
|
|
307
|
+
writeLine(mockChild, { type: 'turn.completed' });
|
|
308
|
+
endProcess(mockChild, 0);
|
|
309
|
+
}, 10);
|
|
310
|
+
const events = await collectEvents(handle.stream);
|
|
311
|
+
const systemEvent = events.find(e => e.type === 'system' && 'subtype' in e && e.subtype === 'raw_output');
|
|
312
|
+
expect(systemEvent).toMatchObject({
|
|
313
|
+
type: 'system',
|
|
314
|
+
subtype: 'raw_output',
|
|
315
|
+
message: 'Spring AI Agent v0.1.0 starting...',
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
describe('SpringAiToolPermissionAdapter integration', () => {
|
|
321
|
+
it('factory returns SpringAiToolPermissionAdapter for spring-ai', () => {
|
|
322
|
+
const adapter = createToolPermissionAdapter('spring-ai');
|
|
323
|
+
expect(adapter).toBeInstanceOf(SpringAiToolPermissionAdapter);
|
|
324
|
+
});
|
|
325
|
+
it('translates full permission set for Spring AI agent', () => {
|
|
326
|
+
const adapter = createToolPermissionAdapter('spring-ai');
|
|
327
|
+
const result = adapter.translatePermissions([
|
|
328
|
+
{ shell: 'pnpm *' },
|
|
329
|
+
{ shell: 'git commit *' },
|
|
330
|
+
{ shell: 'gh pr *' },
|
|
331
|
+
'user-input',
|
|
332
|
+
'Read',
|
|
333
|
+
'Write',
|
|
334
|
+
]);
|
|
335
|
+
expect(result).toEqual([
|
|
336
|
+
'spring-tool:shell:pnpm *',
|
|
337
|
+
'spring-tool:shell:git commit *',
|
|
338
|
+
'spring-tool:shell:gh pr *',
|
|
339
|
+
'user-input',
|
|
340
|
+
'Read',
|
|
341
|
+
'Write',
|
|
342
|
+
]);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
describe('createProvider integration', () => {
|
|
346
|
+
it('creates SpringAiProvider via factory', async () => {
|
|
347
|
+
const { createProvider } = await import('./index.js');
|
|
348
|
+
const provider = createProvider('spring-ai');
|
|
349
|
+
expect(provider.name).toBe('spring-ai');
|
|
350
|
+
});
|
|
351
|
+
});
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spring AI Agent Provider
|
|
3
|
+
*
|
|
4
|
+
* Spawns a Spring AI agent JAR as a child process and parses its JSONL event
|
|
5
|
+
* stream into normalized AgentEvents.
|
|
6
|
+
*
|
|
7
|
+
* CLI invocation patterns:
|
|
8
|
+
* New session: java -jar <JAR> --prompt "<prompt>" --cwd <cwd> --json
|
|
9
|
+
* Resume: java -jar <JAR> --resume <sessionId> --prompt "<prompt>" --cwd <cwd> --json
|
|
10
|
+
*
|
|
11
|
+
* JSONL event types:
|
|
12
|
+
* session.started → init (sessionId)
|
|
13
|
+
* turn.started → system (turn_started)
|
|
14
|
+
* turn.completed → result (success, usage)
|
|
15
|
+
* turn.failed → result (failure)
|
|
16
|
+
* tool.invocation → tool_use
|
|
17
|
+
* tool.result → tool_result
|
|
18
|
+
* assistant.message → assistant_text
|
|
19
|
+
* error → error
|
|
20
|
+
*/
|
|
21
|
+
import { spawn } from 'child_process';
|
|
22
|
+
import { createInterface } from 'readline';
|
|
23
|
+
/**
|
|
24
|
+
* Map a single Spring AI JSONL event to one or more normalized AgentEvents.
|
|
25
|
+
* Exported for unit testing — the AgentHandle uses this internally.
|
|
26
|
+
*/
|
|
27
|
+
export function mapSpringAiEvent(event, state) {
|
|
28
|
+
switch (event.type) {
|
|
29
|
+
case 'session.started':
|
|
30
|
+
state.sessionId = event.session_id;
|
|
31
|
+
return [{
|
|
32
|
+
type: 'init',
|
|
33
|
+
sessionId: event.session_id,
|
|
34
|
+
raw: event,
|
|
35
|
+
}];
|
|
36
|
+
case 'turn.started':
|
|
37
|
+
state.turnCount++;
|
|
38
|
+
return [{
|
|
39
|
+
type: 'system',
|
|
40
|
+
subtype: 'turn_started',
|
|
41
|
+
message: `Turn ${state.turnCount} started`,
|
|
42
|
+
raw: event,
|
|
43
|
+
}];
|
|
44
|
+
case 'turn.completed':
|
|
45
|
+
if (event.usage) {
|
|
46
|
+
state.totalInputTokens += event.usage.input_tokens ?? 0;
|
|
47
|
+
state.totalOutputTokens += event.usage.output_tokens ?? 0;
|
|
48
|
+
}
|
|
49
|
+
return [{
|
|
50
|
+
type: 'result',
|
|
51
|
+
success: true,
|
|
52
|
+
cost: {
|
|
53
|
+
inputTokens: state.totalInputTokens || undefined,
|
|
54
|
+
outputTokens: state.totalOutputTokens || undefined,
|
|
55
|
+
numTurns: state.turnCount || undefined,
|
|
56
|
+
},
|
|
57
|
+
raw: event,
|
|
58
|
+
}];
|
|
59
|
+
case 'turn.failed':
|
|
60
|
+
return [{
|
|
61
|
+
type: 'result',
|
|
62
|
+
success: false,
|
|
63
|
+
errors: [event.error?.message ?? 'Turn failed'],
|
|
64
|
+
errorSubtype: 'turn_failed',
|
|
65
|
+
raw: event,
|
|
66
|
+
}];
|
|
67
|
+
case 'assistant.message':
|
|
68
|
+
return [{
|
|
69
|
+
type: 'assistant_text',
|
|
70
|
+
text: event.text,
|
|
71
|
+
raw: event,
|
|
72
|
+
}];
|
|
73
|
+
case 'tool.invocation':
|
|
74
|
+
return [{
|
|
75
|
+
type: 'tool_use',
|
|
76
|
+
toolName: event.tool_name,
|
|
77
|
+
toolUseId: event.id,
|
|
78
|
+
input: event.input,
|
|
79
|
+
raw: event,
|
|
80
|
+
}];
|
|
81
|
+
case 'tool.result':
|
|
82
|
+
return [{
|
|
83
|
+
type: 'tool_result',
|
|
84
|
+
toolName: event.tool_name,
|
|
85
|
+
toolUseId: event.id,
|
|
86
|
+
content: event.content,
|
|
87
|
+
isError: event.is_error,
|
|
88
|
+
raw: event,
|
|
89
|
+
}];
|
|
90
|
+
case 'error':
|
|
91
|
+
return [{
|
|
92
|
+
type: 'error',
|
|
93
|
+
message: event.message ?? 'Unknown error',
|
|
94
|
+
code: event.code,
|
|
95
|
+
raw: event,
|
|
96
|
+
}];
|
|
97
|
+
default:
|
|
98
|
+
return [{
|
|
99
|
+
type: 'system',
|
|
100
|
+
subtype: 'unknown',
|
|
101
|
+
message: `Unhandled Spring AI event type: ${event.type}`,
|
|
102
|
+
raw: event,
|
|
103
|
+
}];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Provider
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
export class SpringAiProvider {
|
|
110
|
+
name = 'spring-ai';
|
|
111
|
+
spawn(config) {
|
|
112
|
+
return this.createHandle(config);
|
|
113
|
+
}
|
|
114
|
+
resume(sessionId, config) {
|
|
115
|
+
return this.createHandle(config, sessionId);
|
|
116
|
+
}
|
|
117
|
+
createHandle(config, resumeSessionId) {
|
|
118
|
+
const abortController = config.abortController;
|
|
119
|
+
// Resolve Java binary — prefer JAVA_BIN env var, then fall back to 'java'
|
|
120
|
+
const javaBin = config.env.JAVA_BIN || process.env.JAVA_BIN || 'java';
|
|
121
|
+
// Resolve the Spring AI agent JAR path
|
|
122
|
+
const jarPath = config.env.SPRING_AI_AGENT_JAR || process.env.SPRING_AI_AGENT_JAR;
|
|
123
|
+
if (!jarPath) {
|
|
124
|
+
return new SpringAiAgentHandle(null, abortController, 'SPRING_AI_AGENT_JAR environment variable is not set');
|
|
125
|
+
}
|
|
126
|
+
// Build args
|
|
127
|
+
const args = ['-jar', jarPath];
|
|
128
|
+
if (resumeSessionId) {
|
|
129
|
+
args.push('--resume', resumeSessionId);
|
|
130
|
+
}
|
|
131
|
+
args.push('--json');
|
|
132
|
+
args.push('--cwd', config.cwd);
|
|
133
|
+
if (config.autonomous) {
|
|
134
|
+
args.push('--autonomous');
|
|
135
|
+
}
|
|
136
|
+
if (config.sandboxEnabled) {
|
|
137
|
+
args.push('--sandbox');
|
|
138
|
+
}
|
|
139
|
+
// Prompt as final arg
|
|
140
|
+
args.push('--prompt', config.prompt);
|
|
141
|
+
// Spawn the Java process
|
|
142
|
+
const child = spawn(javaBin, args, {
|
|
143
|
+
cwd: config.cwd,
|
|
144
|
+
env: {
|
|
145
|
+
...process.env,
|
|
146
|
+
...config.env,
|
|
147
|
+
},
|
|
148
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
149
|
+
});
|
|
150
|
+
config.onProcessSpawned?.(child.pid);
|
|
151
|
+
// Wire up abort
|
|
152
|
+
const abortHandler = () => {
|
|
153
|
+
child.kill('SIGTERM');
|
|
154
|
+
};
|
|
155
|
+
abortController.signal.addEventListener('abort', abortHandler);
|
|
156
|
+
child.once('exit', () => {
|
|
157
|
+
abortController.signal.removeEventListener('abort', abortHandler);
|
|
158
|
+
});
|
|
159
|
+
child.on('error', (err) => {
|
|
160
|
+
console.error('[SpringAiProvider] Child process error:', err.message);
|
|
161
|
+
});
|
|
162
|
+
return new SpringAiAgentHandle(child, abortController);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// AgentHandle implementation
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
class SpringAiAgentHandle {
|
|
169
|
+
sessionId = null;
|
|
170
|
+
child;
|
|
171
|
+
abortController;
|
|
172
|
+
mapperState = {
|
|
173
|
+
sessionId: null,
|
|
174
|
+
totalInputTokens: 0,
|
|
175
|
+
totalOutputTokens: 0,
|
|
176
|
+
turnCount: 0,
|
|
177
|
+
};
|
|
178
|
+
initError;
|
|
179
|
+
constructor(child, abortController, initError) {
|
|
180
|
+
this.child = child;
|
|
181
|
+
this.abortController = abortController;
|
|
182
|
+
this.initError = initError;
|
|
183
|
+
}
|
|
184
|
+
get stream() {
|
|
185
|
+
return this.createEventStream();
|
|
186
|
+
}
|
|
187
|
+
async injectMessage(_text) {
|
|
188
|
+
// Spring AI CLI mode doesn't support mid-session message injection.
|
|
189
|
+
// The prompt is provided at spawn time. Injection would require
|
|
190
|
+
// stopping and resuming with a new prompt.
|
|
191
|
+
throw new Error('Spring AI provider does not support mid-session message injection. ' +
|
|
192
|
+
'Stop and resume with a new prompt instead.');
|
|
193
|
+
}
|
|
194
|
+
async stop() {
|
|
195
|
+
this.abortController.abort();
|
|
196
|
+
}
|
|
197
|
+
async *createEventStream() {
|
|
198
|
+
// Handle init-time errors (e.g., missing JAR path)
|
|
199
|
+
if (this.initError) {
|
|
200
|
+
yield {
|
|
201
|
+
type: 'error',
|
|
202
|
+
message: this.initError,
|
|
203
|
+
raw: null,
|
|
204
|
+
};
|
|
205
|
+
yield {
|
|
206
|
+
type: 'result',
|
|
207
|
+
success: false,
|
|
208
|
+
errors: [this.initError],
|
|
209
|
+
errorSubtype: 'configuration_error',
|
|
210
|
+
raw: null,
|
|
211
|
+
};
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (!this.child) {
|
|
215
|
+
yield {
|
|
216
|
+
type: 'error',
|
|
217
|
+
message: 'Spring AI process was not created',
|
|
218
|
+
raw: null,
|
|
219
|
+
};
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const stdout = this.child.stdout;
|
|
223
|
+
if (!stdout) {
|
|
224
|
+
yield {
|
|
225
|
+
type: 'error',
|
|
226
|
+
message: 'Spring AI process has no stdout',
|
|
227
|
+
raw: null,
|
|
228
|
+
};
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
// Collect stderr for error reporting
|
|
232
|
+
let stderr = '';
|
|
233
|
+
this.child.stderr?.on('data', (chunk) => {
|
|
234
|
+
stderr += chunk.toString();
|
|
235
|
+
});
|
|
236
|
+
// Parse JSONL lines from stdout
|
|
237
|
+
const rl = createInterface({ input: stdout });
|
|
238
|
+
let hasResult = false;
|
|
239
|
+
for await (const line of rl) {
|
|
240
|
+
const trimmed = line.trim();
|
|
241
|
+
if (!trimmed)
|
|
242
|
+
continue;
|
|
243
|
+
let event;
|
|
244
|
+
try {
|
|
245
|
+
event = JSON.parse(trimmed);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
// Non-JSON output — emit as system event
|
|
249
|
+
yield {
|
|
250
|
+
type: 'system',
|
|
251
|
+
subtype: 'raw_output',
|
|
252
|
+
message: trimmed,
|
|
253
|
+
raw: trimmed,
|
|
254
|
+
};
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const mapped = mapSpringAiEvent(event, this.mapperState);
|
|
258
|
+
for (const agentEvent of mapped) {
|
|
259
|
+
if (agentEvent.type === 'init') {
|
|
260
|
+
this.sessionId = this.mapperState.sessionId;
|
|
261
|
+
}
|
|
262
|
+
if (agentEvent.type === 'result') {
|
|
263
|
+
hasResult = true;
|
|
264
|
+
}
|
|
265
|
+
yield agentEvent;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Wait for process exit
|
|
269
|
+
const exitCode = await new Promise((resolve) => {
|
|
270
|
+
if (this.child.exitCode !== null) {
|
|
271
|
+
resolve(this.child.exitCode);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
this.child.once('exit', (code) => resolve(code));
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
// If we never got a result event, synthesize one from exit code
|
|
278
|
+
if (!hasResult) {
|
|
279
|
+
if (exitCode === 0) {
|
|
280
|
+
yield {
|
|
281
|
+
type: 'result',
|
|
282
|
+
success: true,
|
|
283
|
+
cost: {
|
|
284
|
+
inputTokens: this.mapperState.totalInputTokens || undefined,
|
|
285
|
+
outputTokens: this.mapperState.totalOutputTokens || undefined,
|
|
286
|
+
numTurns: this.mapperState.turnCount || undefined,
|
|
287
|
+
},
|
|
288
|
+
raw: { exitCode },
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
const errorMsg = stderr.trim() || `Spring AI process exited with code ${exitCode}`;
|
|
293
|
+
// Detect common errors
|
|
294
|
+
const isJavaMissing = stderr.includes('java: not found') ||
|
|
295
|
+
stderr.includes('JAVA_HOME') ||
|
|
296
|
+
stderr.includes('No such file or directory');
|
|
297
|
+
const isJarMissing = stderr.includes('Unable to access jarfile') ||
|
|
298
|
+
stderr.includes('jarfile');
|
|
299
|
+
yield {
|
|
300
|
+
type: 'result',
|
|
301
|
+
success: false,
|
|
302
|
+
errors: [errorMsg],
|
|
303
|
+
errorSubtype: isJavaMissing ? 'java_not_found'
|
|
304
|
+
: isJarMissing ? 'jar_not_found'
|
|
305
|
+
: 'process_exit',
|
|
306
|
+
raw: { exitCode, stderr },
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Create a new Spring AI provider instance
|
|
314
|
+
*/
|
|
315
|
+
export function createSpringAiProvider() {
|
|
316
|
+
return new SpringAiProvider();
|
|
317
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spring-ai-provider.test.d.ts","sourceRoot":"","sources":["../../../src/providers/spring-ai-provider.test.ts"],"names":[],"mappings":""}
|