@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bluecopa/harness",
3
- "version": "0.1.0-snapshot.34",
3
+ "version": "0.1.0-snapshot.35",
4
4
  "description": "Provider-agnostic TypeScript agent framework",
5
5
  "license": "UNLICENSED",
6
6
  "scripts": {
@@ -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 = await seedPromise;
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
 
@@ -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, [
@@ -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). */