@bluecopa/harness 0.1.0-snapshot.98 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/AGENTS.md +18 -0
  2. package/README.md +117 -212
  3. package/docs/guides/observability.md +32 -0
  4. package/docs/guides/providers.md +51 -0
  5. package/docs/guides/skills.md +25 -0
  6. package/docs/security/skill-sandbox-threat-model.md +20 -0
  7. package/package.json +1 -29
  8. package/src/agent/create-agent.ts +884 -0
  9. package/src/agent/create-tools.ts +33 -0
  10. package/src/agent/step-executor.ts +15 -0
  11. package/src/agent/types.ts +57 -0
  12. package/src/context/llm-compaction-strategy.ts +37 -0
  13. package/src/context/prepare-step.ts +65 -0
  14. package/src/context/token-tracker.ts +26 -0
  15. package/src/extracted/manifest.json +10 -0
  16. package/src/extracted/prompts/compaction.md +5 -0
  17. package/src/extracted/prompts/system.md +5 -0
  18. package/src/extracted/tools.json +82 -0
  19. package/src/hooks/hook-runner.ts +22 -0
  20. package/src/hooks/tool-wrappers.ts +64 -0
  21. package/src/interfaces/compaction-strategy.ts +18 -0
  22. package/src/interfaces/hooks.ts +24 -0
  23. package/src/interfaces/sandbox-provider.ts +29 -0
  24. package/src/interfaces/session-store.ts +48 -0
  25. package/src/interfaces/tool-provider.ts +70 -0
  26. package/src/loop/bridge.ts +363 -0
  27. package/src/loop/context-store.ts +207 -0
  28. package/src/loop/lcm-tool-loop.ts +163 -0
  29. package/src/loop/vercel-agent-loop.ts +279 -0
  30. package/src/observability/context.ts +17 -0
  31. package/src/observability/metrics.ts +27 -0
  32. package/src/observability/otel.ts +105 -0
  33. package/src/observability/tracing.ts +13 -0
  34. package/src/optimization/agent-evaluator.ts +40 -0
  35. package/src/optimization/config-serializer.ts +16 -0
  36. package/src/optimization/optimization-runner.ts +39 -0
  37. package/src/optimization/trace-collector.ts +33 -0
  38. package/src/permissions/permission-manager.ts +34 -0
  39. package/src/providers/composite-tool-provider.ts +72 -0
  40. package/src/providers/control-plane-e2b-executor.ts +218 -0
  41. package/src/providers/e2b-tool-provider.ts +68 -0
  42. package/src/providers/local-tool-provider.ts +190 -0
  43. package/src/providers/skill-sandbox-provider.ts +46 -0
  44. package/src/sessions/file-session-store.ts +61 -0
  45. package/src/sessions/in-memory-session-store.ts +39 -0
  46. package/src/sessions/session-manager.ts +44 -0
  47. package/src/skills/skill-loader.ts +52 -0
  48. package/src/skills/skill-manager.ts +175 -0
  49. package/src/skills/skill-router.ts +99 -0
  50. package/src/skills/skill-types.ts +26 -0
  51. package/src/subagents/subagent-manager.ts +22 -0
  52. package/src/subagents/task-tool.ts +13 -0
  53. package/tests/integration/agent-loop-basic.spec.ts +56 -0
  54. package/tests/integration/agent-skill-default-from-sandbox.spec.ts +66 -0
  55. package/tests/integration/concurrency-single-turn.spec.ts +35 -0
  56. package/tests/integration/otel-metrics-emission.spec.ts +62 -0
  57. package/tests/integration/otel-trace-propagation.spec.ts +48 -0
  58. package/tests/integration/parity-benchmark.spec.ts +45 -0
  59. package/tests/integration/provider-local-smoke.spec.ts +63 -0
  60. package/tests/integration/session-resume.spec.ts +30 -0
  61. package/tests/integration/skill-install-rollback.spec.ts +64 -0
  62. package/tests/integration/skill-sandbox-file-blob.spec.ts +54 -0
  63. package/tests/integration/skills-progressive-disclosure.spec.ts +61 -0
  64. package/tests/integration/streaming-compaction-boundary.spec.ts +43 -0
  65. package/tests/integration/structured-messages-agent.spec.ts +265 -0
  66. package/tests/integration/subagent-isolation.spec.ts +24 -0
  67. package/tests/security/skill-sandbox-isolation.spec.ts +51 -0
  68. package/tests/unit/create-tools-schema-parity.spec.ts +22 -0
  69. package/tests/unit/extracted-manifest.spec.ts +41 -0
  70. package/tests/unit/interfaces-contract.spec.ts +101 -0
  71. package/tests/unit/structured-messages.spec.ts +176 -0
  72. package/tests/unit/token-tracker.spec.ts +22 -0
  73. package/tsconfig.json +14 -0
  74. package/vitest.config.ts +7 -0
  75. package/dist/arc/app-adapter.d.ts +0 -101
  76. package/dist/arc/app-adapter.js +0 -312
  77. package/dist/arc/app-adapter.js.map +0 -1
  78. package/dist/arc/create-arc-agent.d.ts +0 -50
  79. package/dist/arc/create-arc-agent.js +0 -2926
  80. package/dist/arc/create-arc-agent.js.map +0 -1
  81. package/dist/arc/profile-builder.d.ts +0 -49
  82. package/dist/arc/profile-builder.js +0 -163
  83. package/dist/arc/profile-builder.js.map +0 -1
  84. package/dist/loop/vercel-agent-loop.d.ts +0 -99
  85. package/dist/loop/vercel-agent-loop.js +0 -308
  86. package/dist/loop/vercel-agent-loop.js.map +0 -1
  87. package/dist/types-g-3DvSSE.d.ts +0 -745
@@ -0,0 +1,46 @@
1
+ import type { SandboxFileBlob, SandboxProvider, SandboxExecOptions, SandboxExecResult } from '../interfaces/sandbox-provider';
2
+ import { ControlPlaneE2BExecutor } from './control-plane-e2b-executor';
3
+
4
+ export class SkillSandboxProvider implements SandboxProvider {
5
+ constructor(private readonly executor: ControlPlaneE2BExecutor) {}
6
+
7
+ static fromEnv(): SkillSandboxProvider {
8
+ return new SkillSandboxProvider(ControlPlaneE2BExecutor.fromEnv());
9
+ }
10
+
11
+ async initialize(): Promise<void> {
12
+ await this.executor.initialize();
13
+ }
14
+
15
+ async destroy(): Promise<void> {
16
+ await this.executor.destroy();
17
+ }
18
+
19
+ async exec(command: string, options?: SandboxExecOptions): Promise<SandboxExecResult> {
20
+ const result = await this.executor.bash(command, {
21
+ cwd: options?.cwd,
22
+ timeout: options?.timeoutMs
23
+ });
24
+ return {
25
+ exitCode: result.success ? 0 : 1,
26
+ stdout: result.output ?? '',
27
+ stderr: result.error ?? ''
28
+ };
29
+ }
30
+
31
+ async readSandboxFile(path: string): Promise<SandboxFileBlob> {
32
+ const bytes = await this.executor.readFileBytes(path);
33
+ return {
34
+ data: bytes,
35
+ filename: path.split('/').pop() ?? path
36
+ };
37
+ }
38
+
39
+ async writeSandboxFile(path: string, content: SandboxFileBlob): Promise<void> {
40
+ const text = Buffer.from(content.data).toString('utf8');
41
+ const result = await this.executor.writeFile(path, text);
42
+ if (!result.success) {
43
+ throw new Error(result.error ?? `write failed for ${path}`);
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,61 @@
1
+ import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+
4
+ import type { AgentSession, SessionFilter, SessionStore } from '../interfaces/session-store';
5
+
6
+ export class FileSessionStore implements SessionStore {
7
+ constructor(private readonly baseDir: string) {}
8
+
9
+ private pathFor(id: string): string {
10
+ return join(this.baseDir, `${id}.json`);
11
+ }
12
+
13
+ async save(session: AgentSession): Promise<void> {
14
+ await mkdir(this.baseDir, { recursive: true });
15
+ await writeFile(this.pathFor(session.id), JSON.stringify(session, null, 2), 'utf8');
16
+ }
17
+
18
+ async get(id: string): Promise<AgentSession | null> {
19
+ try {
20
+ const raw = await readFile(this.pathFor(id), 'utf8');
21
+ return JSON.parse(raw) as AgentSession;
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ async list(filter?: SessionFilter): Promise<AgentSession[]> {
28
+ await mkdir(this.baseDir, { recursive: true });
29
+ const files = await readdir(this.baseDir);
30
+ const sessions = (
31
+ await Promise.all(
32
+ files
33
+ .filter((file) => file.endsWith('.json'))
34
+ .map(async (file) => {
35
+ const raw = await readFile(join(this.baseDir, file), 'utf8');
36
+ return JSON.parse(raw) as AgentSession;
37
+ })
38
+ )
39
+ ).filter(Boolean);
40
+
41
+ let items = sessions;
42
+ if (filter?.status) {
43
+ items = items.filter((s) => s.status === filter.status);
44
+ }
45
+ if (filter?.scoredOnly) {
46
+ items = items.filter((s) => s.score !== undefined);
47
+ }
48
+ if (filter?.limit) {
49
+ items = items.slice(0, filter.limit);
50
+ }
51
+ return items;
52
+ }
53
+
54
+ async delete(id: string): Promise<void> {
55
+ await rm(this.pathFor(id), { force: true });
56
+ }
57
+
58
+ async count(filter?: SessionFilter): Promise<number> {
59
+ return (await this.list(filter)).length;
60
+ }
61
+ }
@@ -0,0 +1,39 @@
1
+ import type { AgentSession, SessionFilter, SessionStore } from '../interfaces/session-store';
2
+
3
+ export class InMemorySessionStore implements SessionStore {
4
+ private readonly sessions = new Map<string, AgentSession>();
5
+
6
+ async save(session: AgentSession): Promise<void> {
7
+ this.sessions.set(session.id, session);
8
+ }
9
+
10
+ async get(id: string): Promise<AgentSession | null> {
11
+ return this.sessions.get(id) ?? null;
12
+ }
13
+
14
+ async list(filter?: SessionFilter): Promise<AgentSession[]> {
15
+ let items = [...this.sessions.values()];
16
+
17
+ if (filter?.status) {
18
+ items = items.filter((s) => s.status === filter.status);
19
+ }
20
+
21
+ if (filter?.scoredOnly) {
22
+ items = items.filter((s) => s.score !== undefined);
23
+ }
24
+
25
+ if (filter?.limit) {
26
+ items = items.slice(0, filter.limit);
27
+ }
28
+
29
+ return items;
30
+ }
31
+
32
+ async delete(id: string): Promise<void> {
33
+ this.sessions.delete(id);
34
+ }
35
+
36
+ async count(filter?: SessionFilter): Promise<number> {
37
+ return (await this.list(filter)).length;
38
+ }
39
+ }
@@ -0,0 +1,44 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ import type { AgentSession, Message, SessionStore } from '../interfaces/session-store';
4
+
5
+ export class SessionManager {
6
+ constructor(private readonly store: SessionStore) {}
7
+
8
+ async create(initialPrompt: string): Promise<AgentSession> {
9
+ const now = new Date();
10
+ const session: AgentSession = {
11
+ id: randomUUID(),
12
+ createdAt: now,
13
+ updatedAt: now,
14
+ messages: [{ role: 'user', content: initialPrompt }],
15
+ metadata: {},
16
+ status: 'running'
17
+ };
18
+ await this.store.save(session);
19
+ return session;
20
+ }
21
+
22
+ async appendMessage(sessionId: string, message: Message): Promise<AgentSession> {
23
+ const session = await this.store.get(sessionId);
24
+ if (!session) {
25
+ throw new Error(`Session not found: ${sessionId}`);
26
+ }
27
+
28
+ const updated: AgentSession = {
29
+ ...session,
30
+ updatedAt: new Date(),
31
+ messages: [...session.messages, message]
32
+ };
33
+ await this.store.save(updated);
34
+ return updated;
35
+ }
36
+
37
+ async checkpoint(session: AgentSession): Promise<void> {
38
+ await this.store.save({ ...session, updatedAt: new Date() });
39
+ }
40
+
41
+ async resume(sessionId: string): Promise<AgentSession | null> {
42
+ return this.store.get(sessionId);
43
+ }
44
+ }
@@ -0,0 +1,52 @@
1
+ import { readFile } from 'node:fs/promises';
2
+
3
+ import type { SkillDefinition } from './skill-types';
4
+
5
+ function parseFrontmatter(raw: string): { meta: Record<string, string>; body: string } {
6
+ if (!raw.startsWith('---\n')) {
7
+ return { meta: {}, body: raw };
8
+ }
9
+
10
+ const end = raw.indexOf('\n---\n', 4);
11
+ if (end === -1) {
12
+ return { meta: {}, body: raw };
13
+ }
14
+
15
+ const header = raw.slice(4, end).split('\n');
16
+ const meta: Record<string, string> = {};
17
+ for (const line of header) {
18
+ const idx = line.indexOf(':');
19
+ if (idx <= 0) continue;
20
+ const key = line.slice(0, idx).trim();
21
+ const value = line.slice(idx + 1).trim().replace(/^"|"$/g, '');
22
+ meta[key] = value;
23
+ }
24
+
25
+ return {
26
+ meta,
27
+ body: raw.slice(end + 5).trim()
28
+ };
29
+ }
30
+
31
+ export async function loadSkillFromFile(path: string): Promise<SkillDefinition> {
32
+ const raw = await readFile(path, 'utf8');
33
+ const parsed = parseFrontmatter(raw);
34
+ const pythonDeps = parsed.meta.python_deps
35
+ ? parsed.meta.python_deps.split(',').map((item) => item.trim()).filter(Boolean)
36
+ : [];
37
+ const npmDeps = parsed.meta.npm_deps
38
+ ? parsed.meta.npm_deps.split(',').map((item) => item.trim()).filter(Boolean)
39
+ : [];
40
+
41
+ return {
42
+ name: parsed.meta.name ?? path.split('/').slice(-2, -1)[0] ?? 'unknown-skill',
43
+ description: parsed.meta.description ?? 'No description provided',
44
+ path,
45
+ contextMode: parsed.meta.context === 'fork' ? 'fork' : 'inline',
46
+ dependencies: {
47
+ python: pythonDeps,
48
+ npm: npmDeps
49
+ },
50
+ instructions: parsed.body
51
+ };
52
+ }
@@ -0,0 +1,175 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+
4
+ import type { SandboxProvider } from '../interfaces/sandbox-provider';
5
+ import type { HarnessTelemetry } from '../observability/otel';
6
+ import { SkillSandboxProvider } from '../providers/skill-sandbox-provider';
7
+ import { traceStep } from '../observability/tracing';
8
+ import type { SkillDefinition, SkillInvokeResult, SkillSummary } from './skill-types';
9
+ import { loadSkillFromFile } from './skill-loader';
10
+
11
+ export class SkillManager {
12
+ private readonly summaries = new Map<string, SkillSummary>();
13
+ private readonly fullSkills = new Map<string, SkillDefinition>();
14
+ private readonly installState = new Map<string, 'ready' | 'degraded' | 'installing'>();
15
+
16
+ constructor(
17
+ private readonly sandbox: SandboxProvider = SkillSandboxProvider.fromEnv(),
18
+ private readonly telemetry?: HarnessTelemetry
19
+ ) {}
20
+
21
+ registerSummary(skill: SkillSummary): void {
22
+ this.summaries.set(skill.name, skill);
23
+ }
24
+
25
+ getSkillSummaryForPrompt(): SkillSummary[] {
26
+ return [...this.summaries.values()];
27
+ }
28
+
29
+ async discover(skillIndexPath: string): Promise<SkillSummary[]> {
30
+ const raw = await readFile(skillIndexPath, 'utf8');
31
+ const entries = JSON.parse(raw) as Array<{ name: string; description: string; path: string }>;
32
+ for (const entry of entries) {
33
+ this.registerSummary(entry);
34
+ }
35
+ return this.getSkillSummaryForPrompt();
36
+ }
37
+
38
+ private assertSafeSkillPath(path: string): void {
39
+ if (path.includes('..')) {
40
+ throw new Error('unsafe skill path');
41
+ }
42
+ }
43
+
44
+ private extractShellBlocks(instructions: string): string[] {
45
+ const blocks: string[] = [];
46
+ const regex = /```(?:bash|sh|shell)\n([\s\S]*?)```/g;
47
+ let match: RegExpExecArray | null;
48
+ while ((match = regex.exec(instructions)) !== null) {
49
+ const code = (match[1] ?? '').trim();
50
+ if (code.length > 0) {
51
+ blocks.push(code);
52
+ }
53
+ }
54
+ return blocks;
55
+ }
56
+
57
+ async invoke(
58
+ name: string,
59
+ options?: { mode?: 'execute' | 'instructions_only' }
60
+ ): Promise<SkillInvokeResult> {
61
+ return traceStep(this.telemetry, 'skill.exec', { skill: name }, async () => {
62
+ const summary = this.summaries.get(name);
63
+ if (!summary) {
64
+ throw new Error(`unknown skill: ${name}`);
65
+ }
66
+
67
+ this.assertSafeSkillPath(summary.path);
68
+
69
+ let full = this.fullSkills.get(name);
70
+ if (!full) {
71
+ full = await loadSkillFromFile(resolve(summary.path));
72
+ this.fullSkills.set(name, full);
73
+ }
74
+
75
+ const mode = options?.mode ?? 'execute';
76
+ if (mode === 'instructions_only') {
77
+ return {
78
+ skill: summary,
79
+ instructions: full.instructions ?? '',
80
+ execution: {
81
+ attempted: false,
82
+ success: true,
83
+ output: 'instructions_only mode',
84
+ commandsRun: 0
85
+ }
86
+ };
87
+ }
88
+
89
+ const shellBlocks = this.extractShellBlocks(full.instructions ?? '');
90
+ if (shellBlocks.length === 0) {
91
+ return {
92
+ skill: summary,
93
+ instructions: full.instructions ?? '',
94
+ execution: {
95
+ attempted: false,
96
+ success: true,
97
+ output: 'no executable shell blocks found',
98
+ commandsRun: 0
99
+ }
100
+ };
101
+ }
102
+
103
+ if (this.getInstallState(name) === 'unknown') {
104
+ await this.installDependencies(name);
105
+ }
106
+
107
+ let aggregateStdout = '';
108
+ for (const block of shellBlocks) {
109
+ const result = await this.sandbox.exec(block);
110
+ aggregateStdout += result.stdout;
111
+ if (result.exitCode !== 0) {
112
+ return {
113
+ skill: summary,
114
+ instructions: full.instructions ?? '',
115
+ execution: {
116
+ attempted: true,
117
+ success: false,
118
+ output: aggregateStdout,
119
+ error: result.stderr || 'skill block failed',
120
+ commandsRun: shellBlocks.length
121
+ }
122
+ };
123
+ }
124
+ }
125
+
126
+ return {
127
+ skill: summary,
128
+ instructions: full.instructions ?? '',
129
+ execution: {
130
+ attempted: true,
131
+ success: true,
132
+ output: aggregateStdout,
133
+ commandsRun: shellBlocks.length
134
+ }
135
+ };
136
+ });
137
+ }
138
+
139
+ async installDependencies(name: string): Promise<void> {
140
+ const skill = this.fullSkills.get(name);
141
+ if (!skill) {
142
+ throw new Error(`skill must be invoked before install: ${name}`);
143
+ }
144
+
145
+ this.installState.set(name, 'installing');
146
+
147
+ try {
148
+ const pythonDeps = skill.dependencies?.python ?? [];
149
+ const npmDeps = skill.dependencies?.npm ?? [];
150
+
151
+ for (const dep of pythonDeps) {
152
+ const result = await this.sandbox.exec(`pip install ${dep}`);
153
+ if (result.exitCode !== 0) {
154
+ throw new Error(result.stderr || `pip install failed for ${dep}`);
155
+ }
156
+ }
157
+
158
+ for (const dep of npmDeps) {
159
+ const result = await this.sandbox.exec(`npm install ${dep}`);
160
+ if (result.exitCode !== 0) {
161
+ throw new Error(result.stderr || `npm install failed for ${dep}`);
162
+ }
163
+ }
164
+
165
+ this.installState.set(name, 'ready');
166
+ } catch (error) {
167
+ this.installState.set(name, 'degraded');
168
+ throw error;
169
+ }
170
+ }
171
+
172
+ getInstallState(name: string): 'ready' | 'degraded' | 'installing' | 'unknown' {
173
+ return this.installState.get(name) ?? 'unknown';
174
+ }
175
+ }
@@ -0,0 +1,99 @@
1
+ import { generateObject } from 'ai';
2
+ import { anthropic } from '@ai-sdk/anthropic';
3
+ import { z } from 'zod';
4
+
5
+ import type { SkillSummary } from './skill-types';
6
+
7
+ const routeSchema = z.object({
8
+ skillName: z.string().nullable(),
9
+ confidence: z.number().min(0).max(1),
10
+ rationale: z.string()
11
+ });
12
+
13
+ export interface SkillRouterConfig {
14
+ model?: string;
15
+ minConfidence?: number;
16
+ aliases?: Record<string, string[]>;
17
+ }
18
+
19
+ const DEFAULT_ALIASES: Record<string, string[]> = {
20
+ xlsx: ['excel', 'spreadsheet', 'workbook', 'csv'],
21
+ docx: ['word', 'document', 'doc'],
22
+ pptx: ['powerpoint', 'slides', 'presentation']
23
+ };
24
+
25
+ export class SkillRouter {
26
+ private readonly model: string;
27
+ private readonly minConfidence: number;
28
+ private readonly aliases: Record<string, string[]>;
29
+
30
+ constructor(config: SkillRouterConfig = {}) {
31
+ this.model = config.model ?? process.env.HARNESS_SKILL_ROUTER_MODEL ?? 'claude-3-5-haiku-latest';
32
+ this.minConfidence = config.minConfidence ?? Number(process.env.HARNESS_SKILL_ROUTER_THRESHOLD ?? '0.55');
33
+ this.aliases = {
34
+ ...DEFAULT_ALIASES,
35
+ ...(config.aliases ?? {})
36
+ };
37
+ }
38
+
39
+ async selectSkill(prompt: string, summaries: SkillSummary[]): Promise<SkillSummary | null> {
40
+ if (summaries.length === 0) return null;
41
+ const lower = prompt.toLowerCase();
42
+
43
+ const direct = summaries.find((skill) => this.containsToken(lower, skill.name.toLowerCase()));
44
+ if (direct) return direct;
45
+
46
+ for (const summary of summaries) {
47
+ const aliasList = this.aliases[summary.name.toLowerCase()] ?? [];
48
+ if (aliasList.some((alias) => this.containsToken(lower, alias.toLowerCase()))) {
49
+ return summary;
50
+ }
51
+ }
52
+
53
+ if (!process.env.ANTHROPIC_API_KEY) {
54
+ return null;
55
+ }
56
+
57
+ try {
58
+ const skillList = summaries
59
+ .map((s) => `- ${s.name}: ${s.description}`)
60
+ .join('\n');
61
+
62
+ const { object } = await generateObject({
63
+ model: anthropic(this.model),
64
+ schema: routeSchema,
65
+ system: [
66
+ 'You are a skill router.',
67
+ 'Pick at most one skill name from the provided list.',
68
+ 'Only choose a skill when it clearly helps with the user request.',
69
+ 'If nothing clearly matches, return null skillName and low confidence.'
70
+ ].join(' '),
71
+ prompt: [
72
+ 'User request:',
73
+ prompt,
74
+ '',
75
+ 'Available skills:',
76
+ skillList,
77
+ '',
78
+ 'Return one object with skillName, confidence, rationale.'
79
+ ].join('\n')
80
+ });
81
+
82
+ if (!object.skillName || object.confidence < this.minConfidence) {
83
+ return null;
84
+ }
85
+
86
+ return summaries.find((s) => s.name === object.skillName) ?? null;
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+
92
+ private containsToken(haystack: string, needle: string): boolean {
93
+ if (!needle) return false;
94
+ const escaped = needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
95
+ const regex = new RegExp(`\\b${escaped}\\b`, 'i');
96
+ return regex.test(haystack);
97
+ }
98
+ }
99
+
@@ -0,0 +1,26 @@
1
+ export interface SkillSummary {
2
+ name: string;
3
+ description: string;
4
+ path: string;
5
+ }
6
+
7
+ export interface SkillDefinition extends SkillSummary {
8
+ instructions?: string;
9
+ contextMode?: 'inline' | 'fork';
10
+ dependencies?: {
11
+ python?: string[];
12
+ npm?: string[];
13
+ };
14
+ }
15
+
16
+ export interface SkillInvokeResult {
17
+ skill: SkillSummary;
18
+ instructions: string;
19
+ execution?: {
20
+ attempted: boolean;
21
+ success: boolean;
22
+ output: string;
23
+ error?: string;
24
+ commandsRun?: number;
25
+ };
26
+ }
@@ -0,0 +1,22 @@
1
+ import { createAgent, type AgentRunResult, type AgentRuntime } from '../agent/create-agent';
2
+ import type { HarnessTelemetry } from '../observability/otel';
3
+ import { traceStep } from '../observability/tracing';
4
+
5
+ export interface SubagentManager {
6
+ runIsolated(taskPrompt: string): Promise<AgentRunResult>;
7
+ }
8
+
9
+ export function createSubagentManager(
10
+ runtimeFactory: () => AgentRuntime,
11
+ telemetry?: HarnessTelemetry
12
+ ): SubagentManager {
13
+ return {
14
+ async runIsolated(taskPrompt: string): Promise<AgentRunResult> {
15
+ return traceStep(telemetry, 'subagent.run', { promptLength: taskPrompt.length }, async () => {
16
+ const runtime = runtimeFactory();
17
+ const agent = createAgent(runtime);
18
+ return agent.run(taskPrompt);
19
+ });
20
+ }
21
+ };
22
+ }
@@ -0,0 +1,13 @@
1
+ import type { AgentRunResult } from '../agent/create-agent';
2
+
3
+ export interface TaskTool {
4
+ run(taskPrompt: string): Promise<AgentRunResult>;
5
+ }
6
+
7
+ export function createTaskTool(runSubagent: (taskPrompt: string) => Promise<AgentRunResult>): TaskTool {
8
+ return {
9
+ run(taskPrompt: string) {
10
+ return runSubagent(taskPrompt);
11
+ }
12
+ };
13
+ }
@@ -0,0 +1,56 @@
1
+ import { mkdtemp, rm } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+
6
+ import { createAgent } from '../../src/agent/create-agent';
7
+ import { LocalToolProvider } from '../../src/providers/local-tool-provider';
8
+
9
+ const tempDirs: string[] = [];
10
+
11
+ afterEach(async () => {
12
+ await Promise.all(tempDirs.map((dir) => rm(dir, { recursive: true, force: true })));
13
+ tempDirs.length = 0;
14
+ });
15
+
16
+ describe('agent loop basic', () => {
17
+ it('runs multi-step tool actions and returns final output', async () => {
18
+ const dir = await mkdtemp(join(tmpdir(), 'harness-agent-'));
19
+ tempDirs.push(dir);
20
+
21
+ const provider = new LocalToolProvider(dir);
22
+
23
+ let turn = 0;
24
+ const agent = createAgent({
25
+ toolProvider: provider,
26
+ async nextAction(messages) {
27
+ turn += 1;
28
+ if (turn === 1) {
29
+ return {
30
+ type: 'tool',
31
+ name: 'Write',
32
+ args: { path: 'hello.txt', content: 'hello harness' }
33
+ };
34
+ }
35
+
36
+ if (turn === 2) {
37
+ return {
38
+ type: 'tool',
39
+ name: 'Read',
40
+ args: { path: 'hello.txt' }
41
+ };
42
+ }
43
+
44
+ const toolMessage = [...messages].reverse().find((m: { role: string }) => m.role === 'tool');
45
+ return {
46
+ type: 'final',
47
+ content: `done: ${toolMessage?.content ?? 'missing'}`
48
+ };
49
+ }
50
+ });
51
+
52
+ const result = await agent.run('create file and read it');
53
+ expect(result.output).toContain('done: Read(hello.txt): hello harness');
54
+ expect(result.steps).toBe(3);
55
+ });
56
+ });