@bluecopa/harness 0.1.0-snapshot.34 → 0.1.0-snapshot.35
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 +39 -2
- package/src/arc/arc-loop.ts +2 -0
- package/src/arc/arc-types.ts +2 -0
- package/src/arc/profile-builder.ts +15 -0
- package/src/arc/sig.ts +120 -0
- package/src/arc/stores/rxdb-setup.ts +1 -0
- package/src/arc/types.ts +10 -0
package/package.json
CHANGED
package/src/arc/agent-runner.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { generateText } from 'ai';
|
|
2
|
+
import { generateText, generateObject } from 'ai';
|
|
3
3
|
import { anthropic } from '@ai-sdk/anthropic';
|
|
4
4
|
import type { AgentMessage, ToolCallAction } from '../agent/types';
|
|
5
5
|
import { getTextContent } from '../agent/types';
|
|
@@ -195,6 +195,9 @@ export interface AgentRunnerConfig {
|
|
|
195
195
|
onActivity?: (activity: Activity) => void;
|
|
196
196
|
/** Allowed tool names for executor-level validation (defense-in-depth). */
|
|
197
197
|
allowedToolNames?: string[];
|
|
198
|
+
/** Zod schema for structured output. When set, the terminal step uses generateObject instead of generateText. */
|
|
199
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
200
|
+
outputSchema?: import('zod').ZodObject<any>;
|
|
198
201
|
|
|
199
202
|
// Runtime extras
|
|
200
203
|
hookRunner?: HookRunner;
|
|
@@ -213,6 +216,8 @@ export interface AgentRunResult {
|
|
|
213
216
|
messages: AgentMessage[];
|
|
214
217
|
output: string;
|
|
215
218
|
steps: number;
|
|
219
|
+
/** Structured output from generateObject when outputSchema is set. */
|
|
220
|
+
structuredOutput?: Record<string, unknown>;
|
|
216
221
|
}
|
|
217
222
|
|
|
218
223
|
export class AgentRunner {
|
|
@@ -272,6 +277,24 @@ export class AgentRunner {
|
|
|
272
277
|
if (toolCalls.length === 0) {
|
|
273
278
|
const text = result.text?.trim() ?? 'Done.';
|
|
274
279
|
messages.push({ role: 'assistant', content: text });
|
|
280
|
+
|
|
281
|
+
// Structured output: use generateObject on terminal step when schema is set
|
|
282
|
+
if (config.outputSchema) {
|
|
283
|
+
try {
|
|
284
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
285
|
+
const structured = await (generateObject as any)({
|
|
286
|
+
model: anthropic(config.model),
|
|
287
|
+
schema: config.outputSchema,
|
|
288
|
+
messages: toModelMessages(messages),
|
|
289
|
+
system: [{ role: 'system' as const, content: config.systemPrompt }],
|
|
290
|
+
abortSignal: config.signal,
|
|
291
|
+
});
|
|
292
|
+
return { messages, output: text, steps: step + 1, structuredOutput: structured.object };
|
|
293
|
+
} catch {
|
|
294
|
+
// Structured extraction failed — fall back to text output
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
275
298
|
return { messages, output: text, steps: step + 1 };
|
|
276
299
|
}
|
|
277
300
|
|
|
@@ -388,6 +411,11 @@ export interface CreateProcessConfig {
|
|
|
388
411
|
skillPromptPromise?: Promise<string | null>;
|
|
389
412
|
/** Allowed tool names for executor-level validation (defense-in-depth against hallucinated tool calls). */
|
|
390
413
|
allowedToolNames?: string[];
|
|
414
|
+
/** Zod schema for structured output on the terminal step. */
|
|
415
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
416
|
+
outputSchema?: import('zod').ZodObject<any>;
|
|
417
|
+
/** Few-shot demo messages prepended before context episodes. */
|
|
418
|
+
demoMessages?: AgentMessage[];
|
|
391
419
|
|
|
392
420
|
// Runtime extras
|
|
393
421
|
hookRunner?: HookRunner;
|
|
@@ -441,7 +469,10 @@ export function createProcess(
|
|
|
441
469
|
void (async () => {
|
|
442
470
|
try {
|
|
443
471
|
process.status = 'running';
|
|
444
|
-
const seed =
|
|
472
|
+
const seed = [
|
|
473
|
+
...(config.demoMessages ?? []),
|
|
474
|
+
...(await seedPromise),
|
|
475
|
+
];
|
|
445
476
|
|
|
446
477
|
// Build system prompt: base + optional skill instructions
|
|
447
478
|
let systemPrompt = config.processSystemPrompt ?? PROCESS_SYSTEM_PROMPT;
|
|
@@ -475,6 +506,7 @@ export function createProcess(
|
|
|
475
506
|
'tellUser',
|
|
476
507
|
'downloadRawFile',
|
|
477
508
|
'allowedToolNames',
|
|
509
|
+
'outputSchema',
|
|
478
510
|
]),
|
|
479
511
|
}),
|
|
480
512
|
timeoutPromise(config.processTimeout),
|
|
@@ -496,6 +528,11 @@ export function createProcess(
|
|
|
496
528
|
|
|
497
529
|
const { episode, trace, artifacts } = compressor.compress(compressInput);
|
|
498
530
|
|
|
531
|
+
// Attach structured output from generateObject if present
|
|
532
|
+
if (result.structuredOutput) {
|
|
533
|
+
episode.structuredOutput = result.structuredOutput;
|
|
534
|
+
}
|
|
535
|
+
|
|
499
536
|
await config.episodeStore.addEpisode(episode);
|
|
500
537
|
await config.episodeStore.addTrace(trace);
|
|
501
538
|
|
package/src/arc/arc-loop.ts
CHANGED
|
@@ -578,6 +578,8 @@ export class ArcLoop {
|
|
|
578
578
|
processTools: (profile?.tools ?? globalTools) as any,
|
|
579
579
|
processSystemPrompt: profile?.systemPrompt ?? this.config.processSystemPrompt,
|
|
580
580
|
allowedToolNames: profile?.allowedToolNames,
|
|
581
|
+
outputSchema: profile?.outputSchema,
|
|
582
|
+
demoMessages: profile?.demoMessages,
|
|
581
583
|
skillPromptPromise,
|
|
582
584
|
parentSignal,
|
|
583
585
|
...pickDefined(this.config, [
|
package/src/arc/arc-types.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface Episode {
|
|
|
18
18
|
success: boolean;
|
|
19
19
|
createdAt: number;
|
|
20
20
|
parentEpisodeIds: string[];
|
|
21
|
+
/** Typed output from generateObject when profile has a typed signature. */
|
|
22
|
+
structuredOutput?: Record<string, unknown>;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export interface EpisodeTrace {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { AnyTool } from './arc-types';
|
|
9
9
|
import type { ProfileDeclaration, ProcessProfile, ProfileConfig } from './types';
|
|
10
10
|
import { isProfileDeclaration } from './types';
|
|
11
|
+
import { parseSignature, signatureToSchema, isTypedSignature } from './sig';
|
|
11
12
|
|
|
12
13
|
// ── Thread prompt generation ────────────────────────────────────
|
|
13
14
|
|
|
@@ -120,6 +121,20 @@ export function buildProcessProfile(
|
|
|
120
121
|
};
|
|
121
122
|
if (decl.model) profile.model = decl.model;
|
|
122
123
|
if (decl.maxSteps) profile.maxSteps = decl.maxSteps;
|
|
124
|
+
// Generate output schema from typed signatures (e.g., 'question:string -> evidence:string[]')
|
|
125
|
+
if (isTypedSignature(decl.signature)) {
|
|
126
|
+
const parsed = parseSignature(decl.signature);
|
|
127
|
+
if (parsed.outputs.length > 0) {
|
|
128
|
+
profile.outputSchema = signatureToSchema(parsed);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Render demos as user/assistant message pairs
|
|
132
|
+
if (decl.demos && decl.demos.length > 0) {
|
|
133
|
+
profile.demoMessages = decl.demos.flatMap(demo => [
|
|
134
|
+
{ role: 'user' as const, content: Object.entries(demo.input).map(([k, v]) => `${k}: ${v}`).join('\n') },
|
|
135
|
+
{ role: 'assistant' as const, content: JSON.stringify(demo.output) },
|
|
136
|
+
]);
|
|
137
|
+
}
|
|
123
138
|
return profile;
|
|
124
139
|
}
|
|
125
140
|
|
package/src/arc/sig.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSPy-style signature parsing and schema generation.
|
|
3
|
+
*
|
|
4
|
+
* Parses signature strings like "question:string -> evidence:string[], confidence:number"
|
|
5
|
+
* into typed field definitions, and generates Zod schemas for structured output.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
|
|
10
|
+
// ── Types ──
|
|
11
|
+
|
|
12
|
+
export interface SignatureField {
|
|
13
|
+
name: string;
|
|
14
|
+
type: 'string' | 'number' | 'boolean';
|
|
15
|
+
isArray: boolean;
|
|
16
|
+
isOptional: boolean;
|
|
17
|
+
description?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ParsedSignature {
|
|
21
|
+
inputs: SignatureField[];
|
|
22
|
+
outputs: SignatureField[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── Parser ──
|
|
26
|
+
|
|
27
|
+
const FIELD_RE = /^(\w+)(?::(\w+)(\[\])?)(\?)?(?:\s*\(([^)]+)\))?$/;
|
|
28
|
+
|
|
29
|
+
function parseField(raw: string): SignatureField {
|
|
30
|
+
const trimmed = raw.trim();
|
|
31
|
+
const match = trimmed.match(FIELD_RE);
|
|
32
|
+
|
|
33
|
+
if (!match) {
|
|
34
|
+
// Bare field name — default to string
|
|
35
|
+
const name = trimmed.replace(/\?$/, '');
|
|
36
|
+
return {
|
|
37
|
+
name,
|
|
38
|
+
type: 'string',
|
|
39
|
+
isArray: false,
|
|
40
|
+
isOptional: trimmed.endsWith('?'),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const [, name, typeStr, arrayMark, optionalMark, desc] = match;
|
|
45
|
+
const type = (['string', 'number', 'boolean'].includes(typeStr!) ? typeStr : 'string') as SignatureField['type'];
|
|
46
|
+
|
|
47
|
+
const field: SignatureField = {
|
|
48
|
+
name: name!,
|
|
49
|
+
type,
|
|
50
|
+
isArray: arrayMark === '[]',
|
|
51
|
+
isOptional: optionalMark === '?',
|
|
52
|
+
};
|
|
53
|
+
if (desc) field.description = desc.trim();
|
|
54
|
+
return field;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Parse a signature string into typed input/output fields.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* parseSignature('question:string -> evidence:string[], confidence:number')
|
|
62
|
+
* // { inputs: [{ name: 'question', type: 'string', ... }],
|
|
63
|
+
* // outputs: [{ name: 'evidence', type: 'string', isArray: true, ... },
|
|
64
|
+
* // { name: 'confidence', type: 'number', ... }] }
|
|
65
|
+
*
|
|
66
|
+
* parseSignature('query -> findings')
|
|
67
|
+
* // All fields default to type 'string', isArray false
|
|
68
|
+
*/
|
|
69
|
+
export function parseSignature(sig: string): ParsedSignature {
|
|
70
|
+
const arrowIdx = sig.indexOf('->');
|
|
71
|
+
if (arrowIdx < 0) {
|
|
72
|
+
// No arrow — treat entire string as a single input, single output
|
|
73
|
+
const parts = sig.split(',').map(s => s.trim()).filter(Boolean);
|
|
74
|
+
return {
|
|
75
|
+
inputs: parts.length > 0 ? [parseField(parts[0]!)] : [],
|
|
76
|
+
outputs: parts.length > 1 ? [parseField(parts[1]!)] : [{ name: 'result', type: 'string', isArray: false, isOptional: false }],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const inputStr = sig.slice(0, arrowIdx).trim();
|
|
81
|
+
const outputStr = sig.slice(arrowIdx + 2).trim();
|
|
82
|
+
|
|
83
|
+
const inputs = inputStr.split(',').map(s => s.trim()).filter(Boolean).map(parseField);
|
|
84
|
+
const outputs = outputStr.split(',').map(s => s.trim()).filter(Boolean).map(parseField);
|
|
85
|
+
|
|
86
|
+
return { inputs, outputs };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Schema generation ──
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Convert a parsed signature's output fields into a Zod schema.
|
|
93
|
+
* Used with ai-sdk's generateObject() for structured output extraction.
|
|
94
|
+
*/
|
|
95
|
+
export function signatureToSchema(sig: ParsedSignature): z.ZodObject<Record<string, z.ZodTypeAny>> {
|
|
96
|
+
const shape: Record<string, z.ZodTypeAny> = {};
|
|
97
|
+
|
|
98
|
+
for (const field of sig.outputs) {
|
|
99
|
+
let base: z.ZodTypeAny;
|
|
100
|
+
switch (field.type) {
|
|
101
|
+
case 'number': base = z.number(); break;
|
|
102
|
+
case 'boolean': base = z.boolean(); break;
|
|
103
|
+
default: base = z.string(); break;
|
|
104
|
+
}
|
|
105
|
+
if (field.isArray) base = z.array(base);
|
|
106
|
+
if (field.description) base = base.describe(field.description);
|
|
107
|
+
if (field.isOptional) base = base.optional();
|
|
108
|
+
shape[field.name] = base;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return z.object(shape);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if a signature string has typed fields (contains ':').
|
|
116
|
+
* Untyped signatures like 'query -> findings' are treated as cosmetic labels.
|
|
117
|
+
*/
|
|
118
|
+
export function isTypedSignature(sig: string): boolean {
|
|
119
|
+
return sig.includes(':');
|
|
120
|
+
}
|
|
@@ -22,6 +22,7 @@ const episodeSchema: RxJsonSchema<Episode> = {
|
|
|
22
22
|
success: { type: 'boolean' },
|
|
23
23
|
createdAt: { type: 'integer' },
|
|
24
24
|
parentEpisodeIds: { type: 'array', items: { type: 'string' } },
|
|
25
|
+
structuredOutput: { type: 'object' },
|
|
25
26
|
},
|
|
26
27
|
required: ['id', 'taskId', 'sessionId', 'index', 'threadAction', 'summary', 'model', 'steps', 'success', 'createdAt'],
|
|
27
28
|
indexes: ['taskId', 'sessionId', 'createdAt'],
|
package/src/arc/types.ts
CHANGED
|
@@ -187,6 +187,11 @@ export interface ProfileDeclaration {
|
|
|
187
187
|
maxSteps?: number;
|
|
188
188
|
/** Skill names this profile can use. SkillRouter is constrained to this set. Omit for all skills. */
|
|
189
189
|
skills?: string[];
|
|
190
|
+
/** Few-shot demonstration examples. Rendered as user/assistant message pairs in the prompt. */
|
|
191
|
+
demos?: Array<{
|
|
192
|
+
input: Record<string, string>;
|
|
193
|
+
output: Record<string, unknown>;
|
|
194
|
+
}>;
|
|
190
195
|
}
|
|
191
196
|
|
|
192
197
|
/** A named process profile — runtime form with resolved prompt and tools. */
|
|
@@ -201,6 +206,11 @@ export interface ProcessProfile {
|
|
|
201
206
|
maxSteps?: number;
|
|
202
207
|
/** Allowed tool names for executor-level validation (populated from ProfileDeclaration.tools). */
|
|
203
208
|
allowedToolNames?: string[];
|
|
209
|
+
/** Zod schema for structured output on the terminal step (generated from typed signatures). */
|
|
210
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
211
|
+
outputSchema?: import('zod').ZodObject<any>;
|
|
212
|
+
/** Few-shot demo messages rendered before the task prompt. */
|
|
213
|
+
demoMessages?: import('../agent/types').AgentMessage[];
|
|
204
214
|
}
|
|
205
215
|
|
|
206
216
|
/** A profile config can be either a declaration (new) or a raw ProcessProfile (backward compat). */
|