@studio-foundation/runner 0.3.0-beta.1 → 0.3.0-beta.6
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/package.json +6 -3
- package/ARCHITECTURE.md +0 -53
- package/configs/agents/analyst.agent.yaml +0 -31
- package/configs/agents/code-generator.agent.yaml +0 -31
- package/configs/agents/generic.agent.yaml +0 -23
- package/src/__tests__/script-executor.test.ts +0 -180
- package/src/index.ts +0 -68
- package/src/integrations/integration-loader.test.ts +0 -88
- package/src/integrations/integration-loader.ts +0 -68
- package/src/middleware/anonymization.ts +0 -38
- package/src/plugins/index.ts +0 -4
- package/src/plugins/mcp-client.test.ts +0 -148
- package/src/plugins/mcp-client.ts +0 -128
- package/src/plugins/oauth-provider.test.ts +0 -167
- package/src/plugins/oauth-provider.ts +0 -175
- package/src/plugins/plugin-loader.test.ts +0 -114
- package/src/plugins/plugin-loader.ts +0 -90
- package/src/prompt-builder.test.ts +0 -167
- package/src/prompt-builder.ts +0 -332
- package/src/providers/anthropic.test.ts +0 -101
- package/src/providers/anthropic.ts +0 -135
- package/src/providers/mock.ts +0 -57
- package/src/providers/ollama.test.ts +0 -166
- package/src/providers/ollama.ts +0 -152
- package/src/providers/openai-responses.ts +0 -212
- package/src/providers/openai.test.ts +0 -67
- package/src/providers/openai.ts +0 -139
- package/src/providers/provider.ts +0 -54
- package/src/providers/registry.ts +0 -77
- package/src/runner.test.ts +0 -343
- package/src/runner.ts +0 -396
- package/src/script-executor.ts +0 -107
- package/src/tools/builtin/git.ts +0 -311
- package/src/tools/builtin/patch.ts +0 -257
- package/src/tools/builtin/repo-manager.ts +0 -142
- package/src/tools/builtin/search.ts +0 -108
- package/src/tools/builtin/shell.ts +0 -82
- package/src/tools/builtin/studio-run.ts +0 -73
- package/src/tools/builtin/web-search.test.ts +0 -122
- package/src/tools/builtin/web-search.ts +0 -101
- package/src/tools/errors.test.ts +0 -12
- package/src/tools/errors.ts +0 -6
- package/src/tools/plugin-loader.test.ts +0 -130
- package/src/tools/plugin-loader.ts +0 -203
- package/src/tools/skills/README.md +0 -49
- package/src/tools/skills/skill-loader.test.ts +0 -106
- package/src/tools/skills/skill-loader.ts +0 -62
- package/src/tools/tool-executor.test.ts +0 -88
- package/src/tools/tool-executor.ts +0 -84
- package/src/tools/tool-registry.ts +0 -130
- package/src/tools/yaml-executor.ts +0 -120
- package/src/utils/race-signal.test.ts +0 -50
- package/src/utils/race-signal.ts +0 -17
- package/templates/integrations/linear.integration.yaml +0 -35
- package/templates/integrations/slack.integration.yaml +0 -22
- package/templates/integrations/webhook.integration.yaml +0 -17
- package/templates/tools/git.tool.yaml +0 -80
- package/templates/tools/repo-manager.tool.yaml +0 -64
- package/templates/tools/search.tool.yaml +0 -22
- package/templates/tools/shell.tool.yaml +0 -19
- package/templates/tools/web-search.tool.yaml +0 -24
- package/tests/anonymization-middleware.test.ts +0 -61
- package/tests/anthropic.test.ts +0 -87
- package/tests/apply-patch.test.ts +0 -355
- package/tests/fixtures/tools/test-builtin.tool.yaml +0 -14
- package/tests/fixtures/tools/test-shell.tool.yaml +0 -19
- package/tests/mock-provider.test.ts +0 -104
- package/tests/openai.test.ts +0 -72
- package/tests/plugin-loader.test.ts +0 -54
- package/tests/prompt-builder.test.ts +0 -468
- package/tests/runner-anonymization.test.ts +0 -89
- package/tests/runner.test.ts +0 -885
- package/tests/studio-run.test.ts +0 -94
- package/tests/tool-executor.test.ts +0 -115
- package/tests/tool-registry.test.ts +0 -84
- package/tests/yaml-executor.test.ts +0 -76
- package/tsconfig.json +0 -20
- package/vitest.config.ts +0 -7
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@studio-foundation/runner",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.6",
|
|
4
4
|
"description": "Multi-provider LLM agent runner with tool execution",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
7
10
|
"keywords": [
|
|
8
11
|
"llm",
|
|
9
12
|
"agent",
|
|
@@ -18,8 +21,8 @@
|
|
|
18
21
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
19
22
|
"js-yaml": "^4.1.0",
|
|
20
23
|
"openai": "^4.77.0",
|
|
21
|
-
"@studio-foundation/anonymizer": "0.3.0-beta.
|
|
22
|
-
"@studio-foundation/contracts": "0.3.0-beta.
|
|
24
|
+
"@studio-foundation/anonymizer": "0.3.0-beta.6",
|
|
25
|
+
"@studio-foundation/contracts": "0.3.0-beta.6"
|
|
23
26
|
},
|
|
24
27
|
"devDependencies": {
|
|
25
28
|
"@types/js-yaml": "^4.0.9",
|
package/ARCHITECTURE.md
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# @studio-foundation/runner
|
|
2
|
-
|
|
3
|
-
Agent runner multi-provider. Parle aux LLMs, exécute les tools, streame les tokens.
|
|
4
|
-
|
|
5
|
-
## Concept
|
|
6
|
-
|
|
7
|
-
`runAgent()` prend un `AgentConfig` + contexte, appelle le LLM, exécute les tool calls en boucle, retourne un `AgentRunResult` complet avec tous les tool calls trackés et l'usage de tokens.
|
|
8
|
-
|
|
9
|
-
## Règles
|
|
10
|
-
|
|
11
|
-
- Multi-provider : Anthropic, OpenAI, OpenAI Responses API, Mock — même interface `runAgent()`
|
|
12
|
-
- CHAQUE tool call réel est tracké dans `AgentRunResult.tool_calls`
|
|
13
|
-
- Le runner ne valide PAS — c'est le job de ralph
|
|
14
|
-
- Le runner ne retry PAS — c'est le job de ralph
|
|
15
|
-
- Les prompts sont assemblés ici (`prompt-builder.ts`) — le engine ne touche jamais aux prompts
|
|
16
|
-
- Les tools sont enregistrés ici (`ToolRegistry`) — le engine ne sait pas ce qu'est `repo_manager-write_file`
|
|
17
|
-
- Dépend UNIQUEMENT de `@studio-foundation/contracts`
|
|
18
|
-
|
|
19
|
-
## Fichiers clés
|
|
20
|
-
|
|
21
|
-
- `runner.ts` — `runAgent()` fonction principale, deux chemins : agent loop provider et chat completions standard
|
|
22
|
-
- `prompt-builder.ts` — assemblage system prompt + skills + context + task
|
|
23
|
-
- `providers/anthropic.ts` — Claude (prompt caching, 90% cost reduction sur retries)
|
|
24
|
-
- `providers/openai.ts` — OpenAI chat completions
|
|
25
|
-
- `providers/openai-responses.ts` — OpenAI Responses API (provider owns the loop)
|
|
26
|
-
- `providers/mock.ts` — mock provider pour tests sans API keys
|
|
27
|
-
- `providers/registry.ts` — `ProviderRegistry`, `createDefaultRegistry()`
|
|
28
|
-
- `tools/tool-registry.ts` — `ToolRegistry` (register, lookup)
|
|
29
|
-
- `tools/tool-executor.ts` — `ToolExecutor` (dispatch tool calls)
|
|
30
|
-
- `tools/plugin-loader.ts` — charge les `.tool.yaml` plugins
|
|
31
|
-
- `tools/builtin/repo-manager.ts` — `repo_manager-read_file`, `write_file`, `list_files`
|
|
32
|
-
- `tools/builtin/shell.ts` — `shell-run_command`
|
|
33
|
-
- `tools/builtin/search.ts` — `search-search_codebase`
|
|
34
|
-
- `tools/builtin/git.ts` — `git-checkout`, `git-commit`, `git-push`, `git-pull`, `git-status`, `git-diff`
|
|
35
|
-
- `tools/builtin/patch.ts` — `patch-apply_patch`
|
|
36
|
-
- `tools/builtin/studio-run.ts` — `studio_run` (spawn sous-pipeline, requiert `RunSpawner`)
|
|
37
|
-
- `tools/skills/skill-loader.ts` — charge les `.skill.md` files
|
|
38
|
-
- `middleware/anonymization.ts` — `AnonymizationMiddleware` (wraps `@studio-foundation/anonymizer`)
|
|
39
|
-
- `plugins/plugin-loader.ts` — charge les plugins Claude Code (`.mcp.json` + `skills/`)
|
|
40
|
-
- `plugins/mcp-client.ts` — `MCPClient` (connexion aux serveurs MCP)
|
|
41
|
-
- `integrations/integration-loader.ts` — charge les `.integration.yaml`
|
|
42
|
-
|
|
43
|
-
## Hooks callbacks
|
|
44
|
-
|
|
45
|
-
`RunnerCallbacks` (dans `@studio-foundation/contracts`) wire les hooks du engine vers le runner :
|
|
46
|
-
|
|
47
|
-
- `onPreToolUse` — peut bloquer un tool call (`{ blocked: true }`)
|
|
48
|
-
- `onPostToolUse` — peut injecter du feedback (`{ append_message: '...' }`)
|
|
49
|
-
- `onToolCallStart/Complete`, `onAgentThinking/Progress/Token` — observabilité
|
|
50
|
-
|
|
51
|
-
## Anti-pattern : LE THÉÂTRE
|
|
52
|
-
|
|
53
|
-
Le problème fondamental : les agents génèrent du JSON décrivant des actions au lieu de FAIRE les actions (`tool_calls: 0`). Le runner DOIT tracker tous les tool calls réels. La validation du théâtre est dans ralph, mais le runner fournit les données.
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
name: analyst
|
|
2
|
-
version: 1.0.0
|
|
3
|
-
description: Agent specialized in code analysis and understanding
|
|
4
|
-
|
|
5
|
-
systemMessage: |
|
|
6
|
-
You are an expert code analyst that helps understand complex codebases.
|
|
7
|
-
|
|
8
|
-
When analyzing code:
|
|
9
|
-
- Identify key patterns and architectural decisions
|
|
10
|
-
- Highlight potential issues or improvements
|
|
11
|
-
- Explain complex logic clearly
|
|
12
|
-
- Trace data flow and dependencies
|
|
13
|
-
- Provide actionable insights
|
|
14
|
-
|
|
15
|
-
Use search and file reading tools to explore the codebase thoroughly.
|
|
16
|
-
|
|
17
|
-
capabilities:
|
|
18
|
-
- code_analysis
|
|
19
|
-
- architecture_review
|
|
20
|
-
- dependency_tracing
|
|
21
|
-
- pattern_recognition
|
|
22
|
-
- documentation
|
|
23
|
-
|
|
24
|
-
tools:
|
|
25
|
-
- repo_manager
|
|
26
|
-
- search
|
|
27
|
-
- shell
|
|
28
|
-
|
|
29
|
-
parameters:
|
|
30
|
-
temperature: 0.5
|
|
31
|
-
maxTokens: 4096
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
name: code-generator
|
|
2
|
-
version: 1.0.0
|
|
3
|
-
description: Agent specialized in generating and modifying code
|
|
4
|
-
|
|
5
|
-
systemMessage: |
|
|
6
|
-
You are an expert software engineer that writes clean, efficient, and well-documented code.
|
|
7
|
-
|
|
8
|
-
When generating code:
|
|
9
|
-
- Follow best practices and conventions for the target language
|
|
10
|
-
- Include appropriate error handling
|
|
11
|
-
- Add clear comments and documentation
|
|
12
|
-
- Use modern language features appropriately
|
|
13
|
-
- Ensure code is production-ready
|
|
14
|
-
|
|
15
|
-
Use the available tools to read existing code, write new files, and search the codebase.
|
|
16
|
-
|
|
17
|
-
capabilities:
|
|
18
|
-
- code_generation
|
|
19
|
-
- code_modification
|
|
20
|
-
- refactoring
|
|
21
|
-
- file_operations
|
|
22
|
-
- code_analysis
|
|
23
|
-
|
|
24
|
-
tools:
|
|
25
|
-
- repo_manager
|
|
26
|
-
- search
|
|
27
|
-
- shell
|
|
28
|
-
|
|
29
|
-
parameters:
|
|
30
|
-
temperature: 0.3
|
|
31
|
-
maxTokens: 4096
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
name: generic
|
|
2
|
-
version: 1.0.0
|
|
3
|
-
description: General-purpose assistant agent
|
|
4
|
-
|
|
5
|
-
systemMessage: |
|
|
6
|
-
You are a helpful assistant that can perform tasks using available tools.
|
|
7
|
-
Be concise and accurate in your responses.
|
|
8
|
-
Use tools when necessary to accomplish the user's goals.
|
|
9
|
-
|
|
10
|
-
capabilities:
|
|
11
|
-
- general_qa
|
|
12
|
-
- file_operations
|
|
13
|
-
- command_execution
|
|
14
|
-
- code_search
|
|
15
|
-
|
|
16
|
-
tools:
|
|
17
|
-
- repo_manager
|
|
18
|
-
- shell
|
|
19
|
-
- search
|
|
20
|
-
|
|
21
|
-
parameters:
|
|
22
|
-
temperature: 0.7
|
|
23
|
-
maxTokens: 2048
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import * as cp from 'node:child_process';
|
|
3
|
-
import * as fs from 'node:fs';
|
|
4
|
-
import { EventEmitter } from 'node:events';
|
|
5
|
-
|
|
6
|
-
// Mock child_process and fs before importing the module under test
|
|
7
|
-
vi.mock('node:child_process');
|
|
8
|
-
vi.mock('node:fs');
|
|
9
|
-
|
|
10
|
-
import { runScript } from '../script-executor.js';
|
|
11
|
-
import type { AgentContext } from '../prompt-builder.js';
|
|
12
|
-
|
|
13
|
-
function makeContext(overrides?: Partial<AgentContext>): AgentContext {
|
|
14
|
-
return {
|
|
15
|
-
input: 'test input',
|
|
16
|
-
...overrides,
|
|
17
|
-
} as AgentContext;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function makeSpawnMock(opts: {
|
|
21
|
-
stdout?: string;
|
|
22
|
-
stderr?: string;
|
|
23
|
-
exitCode?: number;
|
|
24
|
-
errorEvent?: Error;
|
|
25
|
-
}) {
|
|
26
|
-
const proc = new EventEmitter() as ReturnType<typeof cp.spawn>;
|
|
27
|
-
(proc as any).stdout = new EventEmitter();
|
|
28
|
-
(proc as any).stderr = new EventEmitter();
|
|
29
|
-
(proc as any).stdin = { write: vi.fn(), end: vi.fn() };
|
|
30
|
-
(proc as any).kill = vi.fn();
|
|
31
|
-
|
|
32
|
-
vi.mocked(cp.spawn).mockReturnValue(proc as any);
|
|
33
|
-
|
|
34
|
-
// Simulate async process lifecycle
|
|
35
|
-
setTimeout(() => {
|
|
36
|
-
if (opts.errorEvent) {
|
|
37
|
-
proc.emit('error', opts.errorEvent);
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
if (opts.stdout) (proc as any).stdout.emit('data', Buffer.from(opts.stdout));
|
|
41
|
-
if (opts.stderr) (proc as any).stderr.emit('data', Buffer.from(opts.stderr));
|
|
42
|
-
proc.emit('close', opts.exitCode ?? 0);
|
|
43
|
-
}, 10);
|
|
44
|
-
|
|
45
|
-
return proc;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
beforeEach(() => {
|
|
49
|
-
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
50
|
-
vi.clearAllMocks();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe('runScript', () => {
|
|
54
|
-
it('parses stdout JSON and returns output on exit 0', async () => {
|
|
55
|
-
const output = { result: 'ok', count: 42 };
|
|
56
|
-
makeSpawnMock({ stdout: JSON.stringify(output), exitCode: 0 });
|
|
57
|
-
|
|
58
|
-
const result = await runScript({
|
|
59
|
-
scriptPath: 'scripts/parse.py',
|
|
60
|
-
runtime: 'python',
|
|
61
|
-
context: makeContext(),
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
expect(result.output).toEqual(output);
|
|
65
|
-
expect(result.tool_calls).toEqual([]);
|
|
66
|
-
expect(result.tool_calls_count).toBe(0);
|
|
67
|
-
expect(result.error).toBeUndefined();
|
|
68
|
-
expect(result.duration_ms).toBeGreaterThanOrEqual(0);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('sets error when script exits with non-zero code', async () => {
|
|
72
|
-
makeSpawnMock({ stdout: '', stderr: 'FileNotFoundError', exitCode: 1 });
|
|
73
|
-
|
|
74
|
-
const result = await runScript({
|
|
75
|
-
scriptPath: 'scripts/parse.py',
|
|
76
|
-
runtime: 'python',
|
|
77
|
-
context: makeContext(),
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
expect(result.output).toBeNull();
|
|
81
|
-
expect(result.error).toMatch(/exited with code 1/);
|
|
82
|
-
expect(result.error).toMatch(/FileNotFoundError/);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('sets error when stdout is not valid JSON', async () => {
|
|
86
|
-
makeSpawnMock({ stdout: 'not json at all', exitCode: 0 });
|
|
87
|
-
|
|
88
|
-
const result = await runScript({
|
|
89
|
-
scriptPath: 'scripts/parse.py',
|
|
90
|
-
runtime: 'python',
|
|
91
|
-
context: makeContext(),
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
expect(result.output).toBeNull();
|
|
95
|
-
expect(result.error).toMatch(/not valid JSON/);
|
|
96
|
-
expect(result.error).toMatch(/not json at all/);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('sets error on process spawn error', async () => {
|
|
100
|
-
const proc = new EventEmitter() as ReturnType<typeof cp.spawn>;
|
|
101
|
-
(proc as any).stdout = new EventEmitter();
|
|
102
|
-
(proc as any).stderr = new EventEmitter();
|
|
103
|
-
(proc as any).stdin = { write: vi.fn(), end: vi.fn() };
|
|
104
|
-
vi.mocked(cp.spawn).mockReturnValue(proc as any);
|
|
105
|
-
setTimeout(() => proc.emit('error', new Error('ENOENT: python3 not found')), 10);
|
|
106
|
-
|
|
107
|
-
const result = await runScript({
|
|
108
|
-
scriptPath: 'scripts/parse.py',
|
|
109
|
-
runtime: 'python',
|
|
110
|
-
context: makeContext(),
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
expect(result.error).toMatch(/process error/);
|
|
114
|
-
expect(result.error).toMatch(/ENOENT/);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('uses python3 command for python runtime', async () => {
|
|
118
|
-
makeSpawnMock({ stdout: '{}', exitCode: 0 });
|
|
119
|
-
|
|
120
|
-
await runScript({ scriptPath: 'scripts/parse.py', runtime: 'python', context: makeContext() });
|
|
121
|
-
|
|
122
|
-
expect(vi.mocked(cp.spawn)).toHaveBeenCalledWith(
|
|
123
|
-
'python3',
|
|
124
|
-
['scripts/parse.py'],
|
|
125
|
-
expect.objectContaining({ stdio: ['pipe', 'pipe', 'pipe'] }),
|
|
126
|
-
);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('uses node command for node runtime', async () => {
|
|
130
|
-
makeSpawnMock({ stdout: '{}', exitCode: 0 });
|
|
131
|
-
|
|
132
|
-
await runScript({ scriptPath: 'scripts/parse.js', runtime: 'node', context: makeContext() });
|
|
133
|
-
|
|
134
|
-
expect(vi.mocked(cp.spawn)).toHaveBeenCalledWith('node', ['scripts/parse.js'], expect.anything());
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('uses sh command for shell runtime', async () => {
|
|
138
|
-
makeSpawnMock({ stdout: '{}', exitCode: 0 });
|
|
139
|
-
|
|
140
|
-
await runScript({ scriptPath: 'scripts/run.sh', runtime: 'shell', context: makeContext() });
|
|
141
|
-
|
|
142
|
-
expect(vi.mocked(cp.spawn)).toHaveBeenCalledWith('sh', ['scripts/run.sh'], expect.anything());
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('activates venv when venv/ directory exists', async () => {
|
|
146
|
-
vi.mocked(fs.existsSync).mockImplementation((p) => String(p).endsWith('venv'));
|
|
147
|
-
makeSpawnMock({ stdout: '{}', exitCode: 0 });
|
|
148
|
-
|
|
149
|
-
await runScript({
|
|
150
|
-
scriptPath: 'scripts/parse.py',
|
|
151
|
-
runtime: 'python',
|
|
152
|
-
context: makeContext(),
|
|
153
|
-
cwd: '/project',
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
const spawnCall = vi.mocked(cp.spawn).mock.calls[0];
|
|
157
|
-
const spawnEnv = (spawnCall[2] as any).env;
|
|
158
|
-
expect(spawnEnv.VIRTUAL_ENV).toBe('/project/venv');
|
|
159
|
-
expect(spawnEnv.PATH).toContain('/project/venv/bin');
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('writes context JSON to stdin', async () => {
|
|
163
|
-
const stdinMock = { write: vi.fn(), end: vi.fn() };
|
|
164
|
-
const proc = new EventEmitter() as ReturnType<typeof cp.spawn>;
|
|
165
|
-
(proc as any).stdout = new EventEmitter();
|
|
166
|
-
(proc as any).stderr = new EventEmitter();
|
|
167
|
-
(proc as any).stdin = stdinMock;
|
|
168
|
-
vi.mocked(cp.spawn).mockReturnValue(proc as any);
|
|
169
|
-
setTimeout(() => {
|
|
170
|
-
(proc as any).stdout.emit('data', Buffer.from('{"ok":true}'));
|
|
171
|
-
proc.emit('close', 0);
|
|
172
|
-
}, 10);
|
|
173
|
-
|
|
174
|
-
const ctx = makeContext({ additional_context: 'hello world' });
|
|
175
|
-
await runScript({ scriptPath: 'scripts/parse.py', runtime: 'python', context: ctx });
|
|
176
|
-
|
|
177
|
-
expect(stdinMock.write).toHaveBeenCalledWith(JSON.stringify(ctx));
|
|
178
|
-
expect(stdinMock.end).toHaveBeenCalled();
|
|
179
|
-
});
|
|
180
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @studio/runner - Multi-provider LLM agent runner with tool execution
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
// Main runner function
|
|
6
|
-
export { runAgent } from './runner.js';
|
|
7
|
-
export type { RunAgentConfig, AgentRunResult } from './runner.js';
|
|
8
|
-
|
|
9
|
-
// Script executor
|
|
10
|
-
export { runScript } from './script-executor.js';
|
|
11
|
-
export type { ScriptExecutorConfig } from './script-executor.js';
|
|
12
|
-
|
|
13
|
-
// Prompt builder
|
|
14
|
-
export { buildPrompt } from './prompt-builder.js';
|
|
15
|
-
export type {
|
|
16
|
-
TaskInput,
|
|
17
|
-
AgentContext,
|
|
18
|
-
GroupFeedbackContext,
|
|
19
|
-
ExecutionContext,
|
|
20
|
-
PromptBuildConfig
|
|
21
|
-
} from './prompt-builder.js';
|
|
22
|
-
|
|
23
|
-
// Providers
|
|
24
|
-
export type { Provider, AgentLoopProvider, AgentLoopResult } from './providers/provider.js';
|
|
25
|
-
export { isAgentLoopProvider } from './providers/provider.js';
|
|
26
|
-
export { OpenAIProvider } from './providers/openai.js';
|
|
27
|
-
export { OpenAIResponsesProvider } from './providers/openai-responses.js';
|
|
28
|
-
export { AnthropicProvider } from './providers/anthropic.js';
|
|
29
|
-
export { OllamaProvider } from './providers/ollama.js';
|
|
30
|
-
export { ProviderRegistry, createDefaultRegistry } from './providers/registry.js';
|
|
31
|
-
export { MockProvider } from './providers/mock.js';
|
|
32
|
-
export type { MockStageConfig } from './providers/mock.js';
|
|
33
|
-
|
|
34
|
-
// Tools
|
|
35
|
-
export { ToolRegistry } from './tools/tool-registry.js';
|
|
36
|
-
export { ToolExecutor } from './tools/tool-executor.js';
|
|
37
|
-
export type { Tool, ToolResult } from './tools/tool-registry.js';
|
|
38
|
-
|
|
39
|
-
// Builtin tools
|
|
40
|
-
export { createRepoManagerTools } from './tools/builtin/repo-manager.js';
|
|
41
|
-
export { createShellTools } from './tools/builtin/shell.js';
|
|
42
|
-
export { createSearchTools } from './tools/builtin/search.js';
|
|
43
|
-
export { createPatchTools } from './tools/builtin/patch.js';
|
|
44
|
-
export { createGitTools } from './tools/builtin/git.js';
|
|
45
|
-
export { createStudioRunTool, STUDIO_RUN_PROMPT_SNIPPET } from './tools/builtin/studio-run.js';
|
|
46
|
-
export { createWebSearchTools, WEB_SEARCH_PROMPT_SNIPPET } from './tools/builtin/web-search.js';
|
|
47
|
-
|
|
48
|
-
export { loadProjectTools, listAvailableToolTemplates, getBundledToolTemplate, BUILTIN_TOOL_NAMES } from './tools/plugin-loader.js';
|
|
49
|
-
export type { LoadedPlugin } from './tools/plugin-loader.js';
|
|
50
|
-
|
|
51
|
-
// Anonymization middleware
|
|
52
|
-
export { AnonymizationMiddleware } from './middleware/anonymization.js';
|
|
53
|
-
export type { AnonymizerOptions } from '@studio-foundation/anonymizer';
|
|
54
|
-
|
|
55
|
-
// Plugin system (Claude Code plugin compatibility)
|
|
56
|
-
export { loadPlugins, MCPClient, StudioOAuthProvider } from './plugins/index.js';
|
|
57
|
-
export type { PluginManifest, MCPServerDef, SkillContent } from './plugins/index.js';
|
|
58
|
-
|
|
59
|
-
// Skill loader
|
|
60
|
-
export { loadSkills, loadSkill, validateSkillManifest } from './tools/skills/skill-loader.js';
|
|
61
|
-
export type { SkillManifest } from './tools/skills/skill-loader.js';
|
|
62
|
-
|
|
63
|
-
// Integration plugin system
|
|
64
|
-
export {
|
|
65
|
-
getBundledIntegrationTemplate,
|
|
66
|
-
listAvailableIntegrationTemplates,
|
|
67
|
-
loadProjectIntegrations,
|
|
68
|
-
} from './integrations/integration-loader.js';
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
-
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import {
|
|
6
|
-
getBundledIntegrationTemplate,
|
|
7
|
-
listAvailableIntegrationTemplates,
|
|
8
|
-
loadProjectIntegrations,
|
|
9
|
-
} from './integration-loader.js';
|
|
10
|
-
|
|
11
|
-
let tmpDir: string;
|
|
12
|
-
|
|
13
|
-
beforeAll(async () => {
|
|
14
|
-
tmpDir = await mkdtemp(join(tmpdir(), 'studio-integration-loader-test-'));
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
afterAll(async () => {
|
|
18
|
-
await rm(tmpDir, { recursive: true, force: true });
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
describe('getBundledIntegrationTemplate', () => {
|
|
22
|
-
it('returns YAML content for a known bundled integration', async () => {
|
|
23
|
-
const content = await getBundledIntegrationTemplate('linear');
|
|
24
|
-
expect(content).not.toBeNull();
|
|
25
|
-
expect(content).toContain('name: linear');
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('returns null for unknown integration name', async () => {
|
|
29
|
-
const content = await getBundledIntegrationTemplate('doesnotexist');
|
|
30
|
-
expect(content).toBeNull();
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
describe('listAvailableIntegrationTemplates', () => {
|
|
35
|
-
it('returns at least linear, slack, webhook', async () => {
|
|
36
|
-
const templates = await listAvailableIntegrationTemplates();
|
|
37
|
-
const names = templates.map(t => t.name);
|
|
38
|
-
expect(names).toContain('linear');
|
|
39
|
-
expect(names).toContain('slack');
|
|
40
|
-
expect(names).toContain('webhook');
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('each entry has name and description', async () => {
|
|
44
|
-
const templates = await listAvailableIntegrationTemplates();
|
|
45
|
-
for (const t of templates) {
|
|
46
|
-
expect(typeof t.name).toBe('string');
|
|
47
|
-
expect(typeof t.description).toBe('string');
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
describe('loadProjectIntegrations', () => {
|
|
53
|
-
it('returns empty array when directory does not exist', async () => {
|
|
54
|
-
const result = await loadProjectIntegrations('/nonexistent/path');
|
|
55
|
-
expect(result).toEqual([]);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('loads valid .integration.yaml files', async () => {
|
|
59
|
-
const intDir = join(tmpDir, 'integrations');
|
|
60
|
-
await mkdir(intDir, { recursive: true });
|
|
61
|
-
await writeFile(join(intDir, 'test.integration.yaml'), `
|
|
62
|
-
name: test
|
|
63
|
-
version: 1
|
|
64
|
-
description: "Test integration"
|
|
65
|
-
config:
|
|
66
|
-
required:
|
|
67
|
-
- TEST_API_KEY
|
|
68
|
-
test:
|
|
69
|
-
type: http
|
|
70
|
-
endpoint: https://api.test.com/health
|
|
71
|
-
expect:
|
|
72
|
-
status: 200
|
|
73
|
-
`);
|
|
74
|
-
const result = await loadProjectIntegrations(intDir);
|
|
75
|
-
expect(result).toHaveLength(1);
|
|
76
|
-
expect(result[0]!.name).toBe('test');
|
|
77
|
-
expect(result[0]!.config?.required).toEqual(['TEST_API_KEY']);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('ignores non-.integration.yaml files', async () => {
|
|
81
|
-
const intDir = join(tmpDir, 'integrations-mixed');
|
|
82
|
-
await mkdir(intDir, { recursive: true });
|
|
83
|
-
await writeFile(join(intDir, 'readme.txt'), 'hello');
|
|
84
|
-
await writeFile(join(intDir, 'other.yaml'), 'name: other\nversion: 1');
|
|
85
|
-
const result = await loadProjectIntegrations(intDir);
|
|
86
|
-
expect(result).toHaveLength(0);
|
|
87
|
-
});
|
|
88
|
-
});
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
// runner/src/integrations/integration-loader.ts
|
|
2
|
-
import { readdir, readFile } from 'node:fs/promises';
|
|
3
|
-
import { existsSync } from 'node:fs';
|
|
4
|
-
import { resolve } from 'node:path';
|
|
5
|
-
import yaml from 'js-yaml';
|
|
6
|
-
import type { IntegrationPluginDef } from '@studio-foundation/contracts';
|
|
7
|
-
|
|
8
|
-
const BUNDLED_INTEGRATION_TEMPLATES_DIR = resolve(
|
|
9
|
-
__dirname,
|
|
10
|
-
'../../templates/integrations'
|
|
11
|
-
);
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Return the raw YAML content of a bundled integration template by name.
|
|
15
|
-
* Returns null if the integration does not exist in the bundled registry.
|
|
16
|
-
*/
|
|
17
|
-
export async function getBundledIntegrationTemplate(name: string): Promise<string | null> {
|
|
18
|
-
const filePath = resolve(BUNDLED_INTEGRATION_TEMPLATES_DIR, `${name}.integration.yaml`);
|
|
19
|
-
try {
|
|
20
|
-
return await readFile(filePath, 'utf-8');
|
|
21
|
-
} catch {
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* List all integration plugins available for installation from the bundled registry.
|
|
28
|
-
* Returns an array of { name, description } objects.
|
|
29
|
-
*/
|
|
30
|
-
export async function listAvailableIntegrationTemplates(): Promise<{ name: string; description: string }[]> {
|
|
31
|
-
let files: string[];
|
|
32
|
-
try {
|
|
33
|
-
files = (await readdir(BUNDLED_INTEGRATION_TEMPLATES_DIR))
|
|
34
|
-
.filter(f => f.endsWith('.integration.yaml'))
|
|
35
|
-
.sort();
|
|
36
|
-
} catch {
|
|
37
|
-
return [];
|
|
38
|
-
}
|
|
39
|
-
const result: { name: string; description: string }[] = [];
|
|
40
|
-
for (const file of files) {
|
|
41
|
-
const content = await readFile(resolve(BUNDLED_INTEGRATION_TEMPLATES_DIR, file), 'utf-8');
|
|
42
|
-
const def = yaml.load(content) as IntegrationPluginDef;
|
|
43
|
-
result.push({ name: file.replace('.integration.yaml', ''), description: def.description ?? '' });
|
|
44
|
-
}
|
|
45
|
-
return result;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Load all `.integration.yaml` files from a project's integrations directory.
|
|
50
|
-
* Returns an empty array if the directory does not exist.
|
|
51
|
-
*/
|
|
52
|
-
export async function loadProjectIntegrations(integrationsDir: string): Promise<IntegrationPluginDef[]> {
|
|
53
|
-
if (!existsSync(integrationsDir)) return [];
|
|
54
|
-
|
|
55
|
-
let files: string[];
|
|
56
|
-
try {
|
|
57
|
-
files = (await readdir(integrationsDir)).filter(f => f.endsWith('.integration.yaml'));
|
|
58
|
-
} catch {
|
|
59
|
-
return [];
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const result: IntegrationPluginDef[] = [];
|
|
63
|
-
for (const file of files.sort()) {
|
|
64
|
-
const content = await readFile(resolve(integrationsDir, file), 'utf-8');
|
|
65
|
-
result.push(yaml.load(content) as IntegrationPluginDef);
|
|
66
|
-
}
|
|
67
|
-
return result;
|
|
68
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { anonymize, deanonymize } from '@studio-foundation/anonymizer';
|
|
2
|
-
import type { AnonymizerOptions } from '@studio-foundation/anonymizer';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Stateful middleware that anonymizes text before sending to LLM
|
|
6
|
-
* and deanonymizes text after receiving from LLM.
|
|
7
|
-
* Accumulates the keymap across calls for cross-call consistency.
|
|
8
|
-
*/
|
|
9
|
-
export class AnonymizationMiddleware {
|
|
10
|
-
private keymap: Record<string, string> = {};
|
|
11
|
-
private options?: Omit<AnonymizerOptions, 'seedKeymap'>;
|
|
12
|
-
|
|
13
|
-
constructor(options?: Omit<AnonymizerOptions, 'seedKeymap'>) {
|
|
14
|
-
this.options = options;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Anonymize text. Accumulated keymap grows with each call.
|
|
19
|
-
* Same PII value always gets the same token (guaranteed by seedKeymap).
|
|
20
|
-
*/
|
|
21
|
-
anonymize(text: string): string {
|
|
22
|
-
const result = anonymize(text, { ...this.options, seedKeymap: this.keymap });
|
|
23
|
-
// result.keymap is the full accumulated keymap (seed + new entries)
|
|
24
|
-
this.keymap = result.keymap;
|
|
25
|
-
return result.text;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Deanonymize text using accumulated keymap.
|
|
30
|
-
*/
|
|
31
|
-
deanonymize(text: string): string {
|
|
32
|
-
return deanonymize(text, this.keymap);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
getKeymap(): Record<string, string> {
|
|
36
|
-
return { ...this.keymap };
|
|
37
|
-
}
|
|
38
|
-
}
|
package/src/plugins/index.ts
DELETED