@bluecopa/harness 0.1.0-snapshot.33 → 0.1.0-snapshot.34
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 +1 -1
- package/src/arc/agent-runner.ts +27 -12
- package/src/arc/arc-loop.ts +25 -48
- package/src/arc/profile-builder.ts +142 -0
- package/src/arc/skill-resolver.ts +78 -0
- package/src/arc/types.ts +57 -14
package/package.json
CHANGED
package/src/arc/agent-runner.ts
CHANGED
|
@@ -193,6 +193,8 @@ export interface AgentRunnerConfig {
|
|
|
193
193
|
seed?: AgentMessage[];
|
|
194
194
|
inbox?: AsyncIterable<AgentMessage>;
|
|
195
195
|
onActivity?: (activity: Activity) => void;
|
|
196
|
+
/** Allowed tool names for executor-level validation (defense-in-depth). */
|
|
197
|
+
allowedToolNames?: string[];
|
|
196
198
|
|
|
197
199
|
// Runtime extras
|
|
198
200
|
hookRunner?: HookRunner;
|
|
@@ -293,6 +295,22 @@ export class AgentRunner {
|
|
|
293
295
|
toolCallId: tc.toolCallId,
|
|
294
296
|
};
|
|
295
297
|
|
|
298
|
+
// Layer 2: executor-level tool validation (defense-in-depth)
|
|
299
|
+
if (config.allowedToolNames && !config.allowedToolNames.includes(tc.toolName)) {
|
|
300
|
+
const resultText = `ERROR: Tool "${tc.toolName}" is not available in this profile.`;
|
|
301
|
+
messages.push({
|
|
302
|
+
role: 'tool',
|
|
303
|
+
content: resultText,
|
|
304
|
+
toolResults: [{
|
|
305
|
+
toolCallId: tc.toolCallId,
|
|
306
|
+
toolName: tc.toolName,
|
|
307
|
+
result: resultText,
|
|
308
|
+
isError: true,
|
|
309
|
+
}],
|
|
310
|
+
});
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
296
314
|
config.onActivity?.({
|
|
297
315
|
type: 'tool_start',
|
|
298
316
|
name: tc.toolName,
|
|
@@ -368,6 +386,8 @@ export interface CreateProcessConfig {
|
|
|
368
386
|
processSystemPrompt?: string;
|
|
369
387
|
/** Async skill instructions to prepend to system prompt (resolved during process startup). */
|
|
370
388
|
skillPromptPromise?: Promise<string | null>;
|
|
389
|
+
/** Allowed tool names for executor-level validation (defense-in-depth against hallucinated tool calls). */
|
|
390
|
+
allowedToolNames?: string[];
|
|
371
391
|
|
|
372
392
|
// Runtime extras
|
|
373
393
|
hookRunner?: HookRunner;
|
|
@@ -454,6 +474,7 @@ export function createProcess(
|
|
|
454
474
|
'askUser',
|
|
455
475
|
'tellUser',
|
|
456
476
|
'downloadRawFile',
|
|
477
|
+
'allowedToolNames',
|
|
457
478
|
]),
|
|
458
479
|
}),
|
|
459
480
|
timeoutPromise(config.processTimeout),
|
|
@@ -462,7 +483,7 @@ export function createProcess(
|
|
|
462
483
|
const durationMs = Date.now() - startTime;
|
|
463
484
|
const nextIndex = await getNextEpisodeIndex(config.episodeStore, config.taskId);
|
|
464
485
|
|
|
465
|
-
const
|
|
486
|
+
const compressInput = {
|
|
466
487
|
taskId: config.taskId,
|
|
467
488
|
sessionId: config.sessionId,
|
|
468
489
|
index: nextIndex,
|
|
@@ -471,7 +492,9 @@ export function createProcess(
|
|
|
471
492
|
model,
|
|
472
493
|
parentEpisodeIds: request.contextEpisodeIds ?? [],
|
|
473
494
|
success: true,
|
|
474
|
-
}
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
const { episode, trace, artifacts } = compressor.compress(compressInput);
|
|
475
498
|
|
|
476
499
|
await config.episodeStore.addEpisode(episode);
|
|
477
500
|
await config.episodeStore.addTrace(trace);
|
|
@@ -490,18 +513,10 @@ export function createProcess(
|
|
|
490
513
|
outbox.push({ type: 'episode', episode });
|
|
491
514
|
outbox.push({ type: 'done', result: processResult });
|
|
492
515
|
|
|
516
|
+
// Async LLM upgrade for long episodes
|
|
493
517
|
if (episode.steps > 10) {
|
|
494
518
|
const upgradeAc = new AbortController();
|
|
495
|
-
void compressor.compressLLM({
|
|
496
|
-
taskId: config.taskId,
|
|
497
|
-
sessionId: config.sessionId,
|
|
498
|
-
index: nextIndex,
|
|
499
|
-
threadAction: request.action,
|
|
500
|
-
messages: result.messages,
|
|
501
|
-
model,
|
|
502
|
-
parentEpisodeIds: request.contextEpisodeIds ?? [],
|
|
503
|
-
success: true,
|
|
504
|
-
}, upgradeAc.signal).then(async (upgraded) => {
|
|
519
|
+
void compressor.compressLLM(compressInput, upgradeAc.signal).then(async (upgraded) => {
|
|
505
520
|
episode.summary = upgraded.episode.summary;
|
|
506
521
|
await config.episodeStore.addEpisode(episode);
|
|
507
522
|
}).catch(() => { /* template summary remains */ });
|
package/src/arc/arc-loop.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
Turn,
|
|
18
18
|
TraceEvent,
|
|
19
19
|
} from './types';
|
|
20
|
+
import { isProfileDeclaration } from './types';
|
|
20
21
|
import type { ResiliencePolicy, ExecutionContext } from './resilience/types';
|
|
21
22
|
import { toModelMessages, estimateTokens } from './message-convert';
|
|
22
23
|
import { orchestratorTools } from './tools';
|
|
@@ -27,9 +28,9 @@ import { createProcess, firstEvent } from './agent-runner';
|
|
|
27
28
|
import { EpisodeCompressor } from './episode-compressor';
|
|
28
29
|
import { runConsolidation } from './consolidation';
|
|
29
30
|
import { pickDefined } from './utils';
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import
|
|
31
|
+
import type { SkillResolver } from './skill-resolver';
|
|
32
|
+
import { DefaultSkillResolver } from './skill-resolver';
|
|
33
|
+
import { resolveProfile } from './profile-builder';
|
|
33
34
|
|
|
34
35
|
// ── Default orchestrator prompt ──
|
|
35
36
|
|
|
@@ -81,9 +82,7 @@ export class ArcLoop {
|
|
|
81
82
|
/** Maps normalized action text → process ID for dedup. */
|
|
82
83
|
private readonly actionIndex = new Map<string, string>();
|
|
83
84
|
private readonly processListeners: Promise<void>[] = [];
|
|
84
|
-
private readonly
|
|
85
|
-
private skillSummaries: SkillSummary[] | null = null;
|
|
86
|
-
private skillSummariesPromise: Promise<SkillSummary[]> | null = null;
|
|
85
|
+
private readonly skillResolver: SkillResolver | undefined;
|
|
87
86
|
|
|
88
87
|
constructor(config: ArcLoopConfig) {
|
|
89
88
|
this.config = config;
|
|
@@ -124,14 +123,10 @@ export class ArcLoop {
|
|
|
124
123
|
this.resilience = config.resilience;
|
|
125
124
|
this.traceWriter = (config as ArcLoopConfig & { traceWriter?: (event: TraceEvent) => void }).traceWriter;
|
|
126
125
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
.then(fs => fs.readFile(config.skillIndexPath!, 'utf-8'))
|
|
132
|
-
.then(raw => JSON.parse(raw) as SkillSummary[])
|
|
133
|
-
.catch(() => []);
|
|
134
|
-
}
|
|
126
|
+
this.skillResolver = config.skillResolver
|
|
127
|
+
?? (config.skillIndexPath
|
|
128
|
+
? new DefaultSkillResolver({ skillIndexPath: config.skillIndexPath })
|
|
129
|
+
: undefined);
|
|
135
130
|
}
|
|
136
131
|
|
|
137
132
|
private trace(kind: TraceEvent['kind']): void {
|
|
@@ -546,18 +541,28 @@ export class ArcLoop {
|
|
|
546
541
|
// ── Process dispatch ──
|
|
547
542
|
|
|
548
543
|
private dispatch(request: ProcessRequest, parentSignal: AbortSignal): Process {
|
|
549
|
-
const
|
|
544
|
+
const profileConfig = request.profile
|
|
550
545
|
? this.config.processProfiles?.[request.profile]
|
|
551
546
|
: undefined;
|
|
547
|
+
|
|
548
|
+
// Resolve ProfileDeclaration → ProcessProfile (legacy profiles pass through)
|
|
549
|
+
const globalTools = (this.config.processTools ?? builtinTools) as Record<string, import('./arc-types').AnyTool>;
|
|
550
|
+
const profile = profileConfig
|
|
551
|
+
? resolveProfile(profileConfig, globalTools)
|
|
552
|
+
: undefined;
|
|
553
|
+
|
|
552
554
|
const defaultModel = resolveModel(
|
|
553
555
|
profile?.model ?? 'medium',
|
|
554
556
|
this.modelMap,
|
|
555
557
|
this.modelMap.medium,
|
|
556
558
|
);
|
|
557
559
|
|
|
558
|
-
// Resolve skill instructions
|
|
559
|
-
const
|
|
560
|
-
?
|
|
560
|
+
// Resolve skill instructions, constrained to profile's allowed skills (if declared)
|
|
561
|
+
const profileSkills = profileConfig && isProfileDeclaration(profileConfig)
|
|
562
|
+
? profileConfig.skills
|
|
563
|
+
: undefined;
|
|
564
|
+
const skillPromptPromise = this.skillResolver
|
|
565
|
+
? this.skillResolver.resolve(request.action, profileSkills).then(r => r?.systemPrompt ?? null)
|
|
561
566
|
: undefined;
|
|
562
567
|
|
|
563
568
|
const proc = createProcess(request, {
|
|
@@ -570,8 +575,9 @@ export class ArcLoop {
|
|
|
570
575
|
processMaxSteps: profile?.maxSteps ?? this.config.processMaxSteps ?? 20,
|
|
571
576
|
processTimeout: this.config.processTimeout ?? 120_000,
|
|
572
577
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
573
|
-
processTools: (profile?.tools ??
|
|
578
|
+
processTools: (profile?.tools ?? globalTools) as any,
|
|
574
579
|
processSystemPrompt: profile?.systemPrompt ?? this.config.processSystemPrompt,
|
|
580
|
+
allowedToolNames: profile?.allowedToolNames,
|
|
575
581
|
skillPromptPromise,
|
|
576
582
|
parentSignal,
|
|
577
583
|
...pickDefined(this.config, [
|
|
@@ -587,35 +593,6 @@ export class ArcLoop {
|
|
|
587
593
|
return proc;
|
|
588
594
|
}
|
|
589
595
|
|
|
590
|
-
/** Resolve skill instructions for a process action. Returns null if no skill matched. */
|
|
591
|
-
private async resolveSkillPrompt(action: string): Promise<string | null> {
|
|
592
|
-
if (!this.skillRouter || !this.skillSummariesPromise) return null;
|
|
593
|
-
|
|
594
|
-
// Ensure summaries are loaded
|
|
595
|
-
if (!this.skillSummaries) {
|
|
596
|
-
this.skillSummaries = await this.skillSummariesPromise;
|
|
597
|
-
}
|
|
598
|
-
if (this.skillSummaries.length === 0) return null;
|
|
599
|
-
|
|
600
|
-
// Fast match only (keyword + alias, no LLM call)
|
|
601
|
-
const matched = await this.skillRouter.selectSkill(action, this.skillSummaries);
|
|
602
|
-
if (matched) {
|
|
603
|
-
console.log(`[skill-router] Matched skill "${matched.name}" for action: "${action.slice(0, 80)}..."`);
|
|
604
|
-
} else {
|
|
605
|
-
console.log(`[skill-router] No match for action: "${action.slice(0, 80)}..."`);
|
|
606
|
-
}
|
|
607
|
-
if (!matched) return null;
|
|
608
|
-
|
|
609
|
-
try {
|
|
610
|
-
const skill = await loadSkillFromFile(matched.path);
|
|
611
|
-
console.log(`[skill-router] Loaded skill "${matched.name}" (${skill.instructions?.length ?? 0} chars)`);
|
|
612
|
-
return skill.instructions || null;
|
|
613
|
-
} catch (err) {
|
|
614
|
-
console.error(`[skill-router] Failed to load skill "${matched.name}":`, err);
|
|
615
|
-
return null;
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
|
|
619
596
|
private traceProcessRunning(procId: string): void {
|
|
620
597
|
if (!this.tracedRunning.has(procId)) {
|
|
621
598
|
this.tracedRunning.add(procId);
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile builder — generates prompts and resolves tools from ProfileDeclarations.
|
|
3
|
+
*
|
|
4
|
+
* The core idea: profiles declare WHAT (signature, tools, background).
|
|
5
|
+
* This module generates the HOW (system prompts, tool objects).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AnyTool } from './arc-types';
|
|
9
|
+
import type { ProfileDeclaration, ProcessProfile, ProfileConfig } from './types';
|
|
10
|
+
import { isProfileDeclaration } from './types';
|
|
11
|
+
|
|
12
|
+
// ── Thread prompt generation ────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate a thread system prompt from a ProfileDeclaration.
|
|
16
|
+
*
|
|
17
|
+
* The prompt tells the thread:
|
|
18
|
+
* - What it is (name)
|
|
19
|
+
* - What it produces (signature)
|
|
20
|
+
* - What tools it has (enforced, not suggested)
|
|
21
|
+
* - Domain knowledge (background)
|
|
22
|
+
*/
|
|
23
|
+
export function buildProfilePrompt(decl: ProfileDeclaration): string {
|
|
24
|
+
const [input, output] = decl.signature.split('->').map(s => s.trim());
|
|
25
|
+
const lines = [
|
|
26
|
+
`You are a ${decl.name} thread.`,
|
|
27
|
+
`Your task: take ${input} and produce ${output}.`,
|
|
28
|
+
'',
|
|
29
|
+
`## Available tools`,
|
|
30
|
+
`You have access to: ${decl.tools.join(', ')}`,
|
|
31
|
+
'Use ONLY these tools. You cannot use any other tools.',
|
|
32
|
+
'If you have Skill Instructions below, follow them for delivery method and style.',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
if (decl.background) {
|
|
36
|
+
lines.push('', '## Background', decl.background);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return lines.join('\n');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Orchestrator prompt generation ──────────────────────────────
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Generate an orchestrator system prompt from a set of profile declarations.
|
|
46
|
+
*
|
|
47
|
+
* The orchestrator sees profile names, signatures, and tool counts —
|
|
48
|
+
* enough to route correctly. No tool names, no delivery methods.
|
|
49
|
+
*
|
|
50
|
+
* @param profiles - The registered profiles
|
|
51
|
+
* @param preamble - Optional strategy preamble (phasing, rules, context).
|
|
52
|
+
* Composed with the auto-generated profile catalog.
|
|
53
|
+
*/
|
|
54
|
+
export function buildOrchestratorPrompt(
|
|
55
|
+
profiles: Record<string, ProfileConfig>,
|
|
56
|
+
preamble?: string,
|
|
57
|
+
): string {
|
|
58
|
+
const profileList = Object.entries(profiles)
|
|
59
|
+
.map(([name, p]) => {
|
|
60
|
+
if (isProfileDeclaration(p)) {
|
|
61
|
+
return `- **${name}** — ${p.signature} (tools: ${p.tools.length}, ${p.model || 'medium'})`;
|
|
62
|
+
}
|
|
63
|
+
// Legacy ProcessProfile — show name only
|
|
64
|
+
return `- **${name}**`;
|
|
65
|
+
})
|
|
66
|
+
.join('\n');
|
|
67
|
+
|
|
68
|
+
const sections = [
|
|
69
|
+
'You are an orchestrator. Dispatch Thread calls to accomplish tasks.',
|
|
70
|
+
'',
|
|
71
|
+
'## Profiles',
|
|
72
|
+
profileList,
|
|
73
|
+
'',
|
|
74
|
+
'## Thread actions',
|
|
75
|
+
'Describe WHAT to create, not HOW. 2-5 sentences. Threads have tools and skill instructions — they know delivery methods.',
|
|
76
|
+
'ALWAYS set the profile parameter on every Thread call.',
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
if (preamble) {
|
|
80
|
+
// Insert preamble after the identity line
|
|
81
|
+
sections.splice(1, 0, '', preamble);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return sections.join('\n');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Tool resolution ─────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolve tool names to tool objects from the global tool registry.
|
|
91
|
+
* Returns only the tools listed in the declaration.
|
|
92
|
+
*/
|
|
93
|
+
export function resolveProfileTools(
|
|
94
|
+
toolNames: string[],
|
|
95
|
+
globalTools: Record<string, AnyTool>,
|
|
96
|
+
): Record<string, AnyTool> {
|
|
97
|
+
const resolved: Record<string, AnyTool> = {};
|
|
98
|
+
for (const name of toolNames) {
|
|
99
|
+
if (globalTools[name]) {
|
|
100
|
+
resolved[name] = globalTools[name];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return resolved;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── ProfileDeclaration → ProcessProfile conversion ──────────────
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Build a runtime ProcessProfile from a ProfileDeclaration.
|
|
110
|
+
* Generates the system prompt and resolves tool objects.
|
|
111
|
+
*/
|
|
112
|
+
export function buildProcessProfile(
|
|
113
|
+
decl: ProfileDeclaration,
|
|
114
|
+
globalTools: Record<string, AnyTool>,
|
|
115
|
+
): ProcessProfile {
|
|
116
|
+
const profile: ProcessProfile = {
|
|
117
|
+
systemPrompt: buildProfilePrompt(decl),
|
|
118
|
+
tools: resolveProfileTools(decl.tools, globalTools),
|
|
119
|
+
allowedToolNames: decl.tools,
|
|
120
|
+
};
|
|
121
|
+
if (decl.model) profile.model = decl.model;
|
|
122
|
+
if (decl.maxSteps) profile.maxSteps = decl.maxSteps;
|
|
123
|
+
return profile;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Resolve a ProfileConfig (declaration or legacy) into a ProcessProfile.
|
|
128
|
+
* Declarations are built; legacy profiles are passed through.
|
|
129
|
+
*/
|
|
130
|
+
export function resolveProfile(
|
|
131
|
+
config: ProfileConfig,
|
|
132
|
+
globalTools: Record<string, AnyTool>,
|
|
133
|
+
): ProcessProfile {
|
|
134
|
+
if (isProfileDeclaration(config)) {
|
|
135
|
+
return buildProcessProfile(config, globalTools);
|
|
136
|
+
}
|
|
137
|
+
// Backfill allowedToolNames for legacy profiles when tools are defined
|
|
138
|
+
if (config.tools && !config.allowedToolNames) {
|
|
139
|
+
return { ...config, allowedToolNames: Object.keys(config.tools) };
|
|
140
|
+
}
|
|
141
|
+
return config;
|
|
142
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SkillResolver — pluggable interface for resolving skill instructions from action text.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from ArcLoop to satisfy DIP + SRP:
|
|
5
|
+
* - ArcLoop depends on the interface, not on SkillRouter/file loader directly
|
|
6
|
+
* - Tests can mock skill resolution without filesystem
|
|
7
|
+
* - Alternative implementations (remote registries) plug in without touching ArcLoop
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { SkillRouter } from '../skills/skill-router';
|
|
11
|
+
import { loadSkillFromFile } from '../skills/skill-loader';
|
|
12
|
+
import type { SkillSummary } from '../skills/skill-types';
|
|
13
|
+
|
|
14
|
+
// ── Interface ──
|
|
15
|
+
|
|
16
|
+
export interface ResolvedSkill {
|
|
17
|
+
name: string;
|
|
18
|
+
systemPrompt: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SkillResolver {
|
|
22
|
+
resolve(action: string, profileSkills?: string[]): Promise<ResolvedSkill | null>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── Default implementation ──
|
|
26
|
+
|
|
27
|
+
export interface DefaultSkillResolverConfig {
|
|
28
|
+
/** Path to skills-index.json */
|
|
29
|
+
skillIndexPath: string;
|
|
30
|
+
/** Optional SkillRouter config (aliases, model, etc.) */
|
|
31
|
+
routerConfig?: ConstructorParameters<typeof SkillRouter>[0];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class DefaultSkillResolver implements SkillResolver {
|
|
35
|
+
private readonly router: SkillRouter;
|
|
36
|
+
private readonly summariesPromise: Promise<SkillSummary[]>;
|
|
37
|
+
private summaries: SkillSummary[] | null = null;
|
|
38
|
+
|
|
39
|
+
constructor(config: DefaultSkillResolverConfig) {
|
|
40
|
+
this.router = new SkillRouter(config.routerConfig);
|
|
41
|
+
this.summariesPromise = import('node:fs/promises')
|
|
42
|
+
.then(fs => fs.readFile(config.skillIndexPath, 'utf-8'))
|
|
43
|
+
.then(raw => JSON.parse(raw) as SkillSummary[])
|
|
44
|
+
.catch(() => []);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async resolve(action: string, profileSkills?: string[]): Promise<ResolvedSkill | null> {
|
|
48
|
+
if (!this.summaries) {
|
|
49
|
+
this.summaries = await this.summariesPromise;
|
|
50
|
+
}
|
|
51
|
+
if (this.summaries.length === 0) return null;
|
|
52
|
+
|
|
53
|
+
// If profile restricts skills, filter summaries to that set
|
|
54
|
+
const candidates = profileSkills
|
|
55
|
+
? this.summaries.filter(s => profileSkills.includes(s.name))
|
|
56
|
+
: this.summaries;
|
|
57
|
+
|
|
58
|
+
if (candidates.length === 0) return null;
|
|
59
|
+
|
|
60
|
+
const matched = await this.router.selectSkill(action, candidates);
|
|
61
|
+
if (matched) {
|
|
62
|
+
console.log(`[skill-resolver] Matched skill "${matched.name}" for action: "${action.slice(0, 80)}..."`);
|
|
63
|
+
} else {
|
|
64
|
+
console.log(`[skill-resolver] No match for action: "${action.slice(0, 80)}..."`);
|
|
65
|
+
}
|
|
66
|
+
if (!matched) return null;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const skill = await loadSkillFromFile(matched.path);
|
|
70
|
+
console.log(`[skill-resolver] Loaded skill "${matched.name}" (${skill.instructions?.length ?? 0} chars)`);
|
|
71
|
+
if (!skill.instructions) return null;
|
|
72
|
+
return { name: matched.name, systemPrompt: skill.instructions };
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.error(`[skill-resolver] Failed to load skill "${matched.name}":`, err);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
package/src/arc/types.ts
CHANGED
|
@@ -41,14 +41,13 @@ export interface EpisodeArtifact {
|
|
|
41
41
|
// ── ArcLoop v2 Config ──
|
|
42
42
|
|
|
43
43
|
export interface ArcLoopConfig {
|
|
44
|
+
// Orchestrator
|
|
44
45
|
/** Orchestrator model (default: 'claude-opus-4-6'). Accepts a model ID or tier name. */
|
|
45
46
|
model?: string;
|
|
46
47
|
/** Model tier mapping. Override to use different models for fast/medium/strong. */
|
|
47
48
|
modelMap?: Record<import('./arc-types').ModelTier, string>;
|
|
48
49
|
/** Anthropic API key */
|
|
49
50
|
apiKey?: string;
|
|
50
|
-
|
|
51
|
-
// Orchestrator
|
|
52
51
|
/** Custom orchestrator system prompt */
|
|
53
52
|
systemPrompt?: string;
|
|
54
53
|
/** Max orchestrator turns before stopping (default: 30) */
|
|
@@ -57,6 +56,8 @@ export interface ArcLoopConfig {
|
|
|
57
56
|
extraOrchestratorTools?: Record<string, import('./arc-types').AnyTool>;
|
|
58
57
|
/** Handler for extra orchestrator tools. Return an AgentAction (typically FinalAction with directive). */
|
|
59
58
|
onOrchestratorTool?: (name: string, args: Record<string, unknown>) => Promise<import('../agent/types').AgentAction>;
|
|
59
|
+
/** Optional resilience policy applied to LLM calls (retry, circuit breaker, timeout, etc.). */
|
|
60
|
+
resilience?: import('./resilience/types').ResiliencePolicy;
|
|
60
61
|
|
|
61
62
|
// Processes
|
|
62
63
|
/** Per-process timeout in ms (default: 120_000) */
|
|
@@ -65,8 +66,10 @@ export interface ArcLoopConfig {
|
|
|
65
66
|
processMaxSteps?: number;
|
|
66
67
|
/** Default system prompt for all processes (overrides the built-in default) */
|
|
67
68
|
processSystemPrompt?: string;
|
|
68
|
-
/** Named process profiles.
|
|
69
|
-
processProfiles?: Record<string,
|
|
69
|
+
/** Named process profiles. Accepts ProfileDeclaration (new, declarative) or ProcessProfile (legacy). */
|
|
70
|
+
processProfiles?: Record<string, ProfileConfig>;
|
|
71
|
+
/** Tools available inside processes (default: builtinTools) */
|
|
72
|
+
processTools?: Record<string, import('./arc-types').AnyTool>;
|
|
70
73
|
|
|
71
74
|
// Context
|
|
72
75
|
/** Context window size in tokens (default: 200_000) */
|
|
@@ -74,10 +77,6 @@ export interface ArcLoopConfig {
|
|
|
74
77
|
/** Tokens reserved for output (default: 20_000) */
|
|
75
78
|
outputReserve?: number;
|
|
76
79
|
|
|
77
|
-
// Memory
|
|
78
|
-
/** Enable auto-memory detection and promotion (default: true) */
|
|
79
|
-
autoMemory?: boolean;
|
|
80
|
-
|
|
81
80
|
// Stores (required)
|
|
82
81
|
episodeStore: import('./arc-types').EpisodeStore;
|
|
83
82
|
sessionMemoStore: import('./arc-types').SessionMemoStore;
|
|
@@ -87,10 +86,16 @@ export interface ArcLoopConfig {
|
|
|
87
86
|
taskId: string;
|
|
88
87
|
sessionId: string;
|
|
89
88
|
|
|
89
|
+
// Memory
|
|
90
|
+
/** Enable auto-memory detection and promotion (default: true) */
|
|
91
|
+
autoMemory?: boolean;
|
|
92
|
+
|
|
93
|
+
// Dependency injection
|
|
94
|
+
/** Pre-built SkillResolver instance. If omitted, one is created from skillIndexPath (if provided). */
|
|
95
|
+
skillResolver?: import('./skill-resolver').SkillResolver;
|
|
96
|
+
|
|
90
97
|
// Tools & providers
|
|
91
98
|
toolProvider: import('../interfaces/tool-provider').ToolProvider;
|
|
92
|
-
/** Tools available inside processes (default: builtinTools) */
|
|
93
|
-
processTools?: Record<string, import('./arc-types').AnyTool>;
|
|
94
99
|
/** Tool provider for skill-matched processes (sandbox) */
|
|
95
100
|
skillToolProvider?: import('../interfaces/tool-provider').ToolProvider;
|
|
96
101
|
/** Local directory to sync sandbox output artifacts to (default: './outputs') */
|
|
@@ -104,9 +109,6 @@ export interface ArcLoopConfig {
|
|
|
104
109
|
hookRunner?: import('../hooks/hook-runner').HookRunner;
|
|
105
110
|
permissionManager?: import('../permissions/permission-manager').PermissionManager;
|
|
106
111
|
executeToolAction?: (action: import('../agent/types').ToolCallAction) => Promise<import('../interfaces/tool-provider').ToolResult | null>;
|
|
107
|
-
|
|
108
|
-
/** Optional resilience policy applied to LLM calls (retry, circuit breaker, timeout, etc.). */
|
|
109
|
-
resilience?: import('./resilience/types').ResiliencePolicy;
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
// ── Process types ──
|
|
@@ -156,7 +158,38 @@ export interface ProcessRequest {
|
|
|
156
158
|
profile?: string;
|
|
157
159
|
}
|
|
158
160
|
|
|
159
|
-
/**
|
|
161
|
+
/**
|
|
162
|
+
* ProfileDeclaration — typed, declarative profile definition.
|
|
163
|
+
*
|
|
164
|
+
* Replaces hand-written system prompts with structured declarations.
|
|
165
|
+
* The system generates prompts from the declaration and enforces tool constraints.
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* const visual: ProfileDeclaration = {
|
|
169
|
+
* name: 'visual',
|
|
170
|
+
* signature: 'description -> compiledArtifact',
|
|
171
|
+
* tools: ['CompileJsx', 'Read', 'ListFiles'],
|
|
172
|
+
* skills: ['visual-explainer'],
|
|
173
|
+
* };
|
|
174
|
+
*/
|
|
175
|
+
export interface ProfileDeclaration {
|
|
176
|
+
/** Profile name (matches the Thread tool's profile parameter). */
|
|
177
|
+
name: string;
|
|
178
|
+
/** Typed signature: "input -> output". Describes what this profile produces. */
|
|
179
|
+
signature: string;
|
|
180
|
+
/** Tool names this profile can use. Only these tools are available — enforced at schema + executor level. */
|
|
181
|
+
tools: string[];
|
|
182
|
+
/** Domain knowledge injected into system prompt (skill content, color systems, etc.). */
|
|
183
|
+
background?: string;
|
|
184
|
+
/** Default model tier. */
|
|
185
|
+
model?: import('./arc-types').ModelTier;
|
|
186
|
+
/** Max LLM steps. */
|
|
187
|
+
maxSteps?: number;
|
|
188
|
+
/** Skill names this profile can use. SkillRouter is constrained to this set. Omit for all skills. */
|
|
189
|
+
skills?: string[];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** A named process profile — runtime form with resolved prompt and tools. */
|
|
160
193
|
export interface ProcessProfile {
|
|
161
194
|
/** System prompt for processes using this profile. */
|
|
162
195
|
systemPrompt: string;
|
|
@@ -166,6 +199,16 @@ export interface ProcessProfile {
|
|
|
166
199
|
model?: import('./arc-types').ModelTier;
|
|
167
200
|
/** Max steps for this profile (Thread tool's explicit maxSteps overrides this). */
|
|
168
201
|
maxSteps?: number;
|
|
202
|
+
/** Allowed tool names for executor-level validation (populated from ProfileDeclaration.tools). */
|
|
203
|
+
allowedToolNames?: string[];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** A profile config can be either a declaration (new) or a raw ProcessProfile (backward compat). */
|
|
207
|
+
export type ProfileConfig = ProfileDeclaration | ProcessProfile;
|
|
208
|
+
|
|
209
|
+
/** Type guard: check if a profile config is a declaration. */
|
|
210
|
+
export function isProfileDeclaration(p: ProfileConfig): p is ProfileDeclaration {
|
|
211
|
+
return 'signature' in p && typeof (p as ProfileDeclaration).signature === 'string';
|
|
169
212
|
}
|
|
170
213
|
|
|
171
214
|
export type Activity =
|