cognitive-runtime 0.3.0 → 0.5.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.
package/README.md CHANGED
@@ -65,6 +65,45 @@ echo "review this code" | cog pipe --module code-reviewer
65
65
  cog doctor
66
66
  ```
67
67
 
68
+ ## Module Formats
69
+
70
+ ### v2 (Recommended)
71
+
72
+ ```
73
+ my-module/
74
+ ├── module.yaml # Machine-readable manifest
75
+ ├── prompt.md # Human-readable prompt
76
+ ├── schema.json # IO contract
77
+ └── tests/
78
+ ├── case1.input.json
79
+ └── case1.expected.json
80
+ ```
81
+
82
+ **module.yaml**:
83
+ ```yaml
84
+ name: my-module
85
+ version: 2.0.0
86
+ responsibility: What this module does
87
+ constraints:
88
+ no_network: true
89
+ no_side_effects: true
90
+ output:
91
+ mode: json_strict
92
+ require_confidence: true
93
+ require_rationale: true
94
+ require_behavior_equivalence: true
95
+ tools:
96
+ allowed: []
97
+ ```
98
+
99
+ ### v1 (Legacy, still supported)
100
+
101
+ ```
102
+ my-module/
103
+ ├── MODULE.md # Frontmatter + prompt combined
104
+ └── schema.json
105
+ ```
106
+
68
107
  ## Providers
69
108
 
70
109
  | Provider | Environment Variable | Default Model |
package/dist/cli.js CHANGED
@@ -10,7 +10,7 @@
10
10
  import { parseArgs } from 'node:util';
11
11
  import { getProvider, listProviders } from './providers/index.js';
12
12
  import { run, list, pipe, init } from './commands/index.js';
13
- const VERSION = '0.3.0';
13
+ const VERSION = '0.5.0';
14
14
  async function main() {
15
15
  const args = process.argv.slice(2);
16
16
  const command = args[0];
@@ -36,19 +36,21 @@ export async function pipe(ctx, options) {
36
36
  const result = await runModule(module, ctx.provider, {
37
37
  args: inputData ? undefined : input,
38
38
  input: inputData,
39
- validateInput: !options.noValidate,
40
- validateOutput: !options.noValidate,
41
39
  });
42
- // Output JSON to stdout
43
- console.log(JSON.stringify(result.output));
40
+ // Output envelope format to stdout
41
+ console.log(JSON.stringify(result));
44
42
  return {
45
- success: true,
43
+ success: result.ok,
46
44
  data: result,
47
45
  };
48
46
  }
49
47
  catch (e) {
50
48
  const error = e instanceof Error ? e.message : String(e);
51
- console.error(JSON.stringify({ error }));
49
+ // Output error in envelope format
50
+ console.log(JSON.stringify({
51
+ ok: false,
52
+ error: { code: 'RUNTIME_ERROR', message: error }
53
+ }));
52
54
  return {
53
55
  success: false,
54
56
  error,
@@ -30,14 +30,31 @@ export async function run(moduleName, ctx, options = {}) {
30
30
  const result = await runModule(module, ctx.provider, {
31
31
  args: options.args,
32
32
  input: inputData,
33
- validateInput: !options.noValidate,
34
- validateOutput: !options.noValidate,
35
33
  verbose: options.verbose || ctx.verbose,
36
34
  });
37
- return {
38
- success: true,
39
- data: options.pretty ? result : result.output,
40
- };
35
+ // Return envelope format or extracted data
36
+ if (options.pretty) {
37
+ return {
38
+ success: result.ok,
39
+ data: result,
40
+ };
41
+ }
42
+ else {
43
+ // For non-pretty mode, return data (success) or error (failure)
44
+ if (result.ok) {
45
+ return {
46
+ success: true,
47
+ data: result.data,
48
+ };
49
+ }
50
+ else {
51
+ return {
52
+ success: false,
53
+ error: `${result.error?.code}: ${result.error?.message}`,
54
+ data: result.partial_data,
55
+ };
56
+ }
57
+ }
41
58
  }
42
59
  catch (e) {
43
60
  return {
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Exports all public APIs for programmatic use.
5
5
  */
6
- export type { Provider, InvokeParams, InvokeResult, Message, CognitiveModule, ModuleResult, CommandContext, CommandResult, } from './types.js';
6
+ export type { Provider, InvokeParams, InvokeResult, Message, CognitiveModule, ModuleResult, ModuleInput, ModuleConstraints, ToolsPolicy, OutputContract, FailureContract, CommandContext, CommandResult, } from './types.js';
7
7
  export { getProvider, listProviders, GeminiProvider, OpenAIProvider, AnthropicProvider, BaseProvider, } from './providers/index.js';
8
8
  export { loadModule, findModule, listModules, getDefaultSearchPaths, runModule, } from './modules/index.js';
9
9
  export { run, list, pipe } from './commands/index.js';
@@ -1,7 +1,11 @@
1
1
  /**
2
2
  * Module Loader - Load and parse Cognitive Modules
3
+ * Supports both v1 (MODULE.md) and v2 (module.yaml + prompt.md) formats
3
4
  */
4
5
  import type { CognitiveModule } from '../types.js';
6
+ /**
7
+ * Load a Cognitive Module (auto-detects format)
8
+ */
5
9
  export declare function loadModule(modulePath: string): Promise<CognitiveModule>;
6
10
  export declare function findModule(name: string, searchPaths: string[]): Promise<CognitiveModule | null>;
7
11
  export declare function listModules(searchPaths: string[]): Promise<CognitiveModule[]>;
@@ -1,11 +1,80 @@
1
1
  /**
2
2
  * Module Loader - Load and parse Cognitive Modules
3
+ * Supports both v1 (MODULE.md) and v2 (module.yaml + prompt.md) formats
3
4
  */
4
5
  import * as fs from 'node:fs/promises';
5
6
  import * as path from 'node:path';
6
7
  import yaml from 'js-yaml';
7
8
  const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n([\s\S]*))?/;
8
- export async function loadModule(modulePath) {
9
+ /**
10
+ * Detect module format version
11
+ */
12
+ async function detectFormat(modulePath) {
13
+ const v2Manifest = path.join(modulePath, 'module.yaml');
14
+ try {
15
+ await fs.access(v2Manifest);
16
+ return 'v2';
17
+ }
18
+ catch {
19
+ return 'v1';
20
+ }
21
+ }
22
+ /**
23
+ * Load v2 format module (module.yaml + prompt.md)
24
+ */
25
+ async function loadModuleV2(modulePath) {
26
+ const manifestFile = path.join(modulePath, 'module.yaml');
27
+ const promptFile = path.join(modulePath, 'prompt.md');
28
+ const schemaFile = path.join(modulePath, 'schema.json');
29
+ // Read module.yaml
30
+ const manifestContent = await fs.readFile(manifestFile, 'utf-8');
31
+ const manifest = yaml.load(manifestContent);
32
+ // Read prompt.md
33
+ let prompt = '';
34
+ try {
35
+ prompt = await fs.readFile(promptFile, 'utf-8');
36
+ }
37
+ catch {
38
+ // prompt.md is optional, manifest may include inline prompt
39
+ }
40
+ // Read schema.json
41
+ let inputSchema;
42
+ let outputSchema;
43
+ let errorSchema;
44
+ try {
45
+ const schemaContent = await fs.readFile(schemaFile, 'utf-8');
46
+ const schema = JSON.parse(schemaContent);
47
+ inputSchema = schema.input;
48
+ outputSchema = schema.output;
49
+ errorSchema = schema.error;
50
+ }
51
+ catch {
52
+ // Schema file is optional but recommended
53
+ }
54
+ return {
55
+ name: manifest.name || path.basename(modulePath),
56
+ version: manifest.version || '1.0.0',
57
+ responsibility: manifest.responsibility || '',
58
+ excludes: manifest.excludes || [],
59
+ constraints: manifest.constraints,
60
+ policies: manifest.policies,
61
+ tools: manifest.tools,
62
+ output: manifest.output,
63
+ failure: manifest.failure,
64
+ runtimeRequirements: manifest.runtime_requirements,
65
+ context: manifest.context,
66
+ prompt,
67
+ inputSchema,
68
+ outputSchema,
69
+ errorSchema,
70
+ location: modulePath,
71
+ format: 'v2',
72
+ };
73
+ }
74
+ /**
75
+ * Load v1 format module (MODULE.md with frontmatter)
76
+ */
77
+ async function loadModuleV1(modulePath) {
9
78
  const moduleFile = path.join(modulePath, 'MODULE.md');
10
79
  const schemaFile = path.join(modulePath, 'schema.json');
11
80
  // Read MODULE.md
@@ -29,29 +98,66 @@ export async function loadModule(modulePath) {
29
98
  catch {
30
99
  // Schema file is optional
31
100
  }
101
+ // Extract constraints from v1 format
102
+ const constraints = {};
103
+ const v1Constraints = frontmatter.constraints;
104
+ if (v1Constraints) {
105
+ constraints.no_network = v1Constraints.no_network;
106
+ constraints.no_side_effects = v1Constraints.no_side_effects;
107
+ constraints.no_inventing_data = v1Constraints.no_inventing_data;
108
+ }
32
109
  return {
33
110
  name: frontmatter.name || path.basename(modulePath),
34
111
  version: frontmatter.version || '1.0.0',
35
112
  responsibility: frontmatter.responsibility || '',
36
113
  excludes: frontmatter.excludes || [],
114
+ constraints: Object.keys(constraints).length > 0 ? constraints : undefined,
37
115
  context: frontmatter.context,
38
116
  prompt,
39
117
  inputSchema,
40
118
  outputSchema,
41
119
  location: modulePath,
120
+ format: 'v1',
42
121
  };
43
122
  }
123
+ /**
124
+ * Load a Cognitive Module (auto-detects format)
125
+ */
126
+ export async function loadModule(modulePath) {
127
+ const format = await detectFormat(modulePath);
128
+ if (format === 'v2') {
129
+ return loadModuleV2(modulePath);
130
+ }
131
+ else {
132
+ return loadModuleV1(modulePath);
133
+ }
134
+ }
135
+ /**
136
+ * Check if a directory contains a valid module
137
+ */
138
+ async function isValidModule(modulePath) {
139
+ const v2Manifest = path.join(modulePath, 'module.yaml');
140
+ const v1Module = path.join(modulePath, 'MODULE.md');
141
+ try {
142
+ await fs.access(v2Manifest);
143
+ return true;
144
+ }
145
+ catch {
146
+ try {
147
+ await fs.access(v1Module);
148
+ return true;
149
+ }
150
+ catch {
151
+ return false;
152
+ }
153
+ }
154
+ }
44
155
  export async function findModule(name, searchPaths) {
45
156
  for (const basePath of searchPaths) {
46
157
  const modulePath = path.join(basePath, name);
47
- const moduleFile = path.join(modulePath, 'MODULE.md');
48
- try {
49
- await fs.access(moduleFile);
158
+ if (await isValidModule(modulePath)) {
50
159
  return await loadModule(modulePath);
51
160
  }
52
- catch {
53
- // Module not found in this path, continue
54
- }
55
161
  }
56
162
  return null;
57
163
  }
@@ -63,14 +169,14 @@ export async function listModules(searchPaths) {
63
169
  for (const entry of entries) {
64
170
  if (entry.isDirectory()) {
65
171
  const modulePath = path.join(basePath, entry.name);
66
- const moduleFile = path.join(modulePath, 'MODULE.md');
67
- try {
68
- await fs.access(moduleFile);
69
- const module = await loadModule(modulePath);
70
- modules.push(module);
71
- }
72
- catch {
73
- // Not a valid module, skip
172
+ if (await isValidModule(modulePath)) {
173
+ try {
174
+ const module = await loadModule(modulePath);
175
+ modules.push(module);
176
+ }
177
+ catch {
178
+ // Skip invalid modules
179
+ }
74
180
  }
75
181
  }
76
182
  }
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Module Runner - Execute Cognitive Modules
3
+ * v2.1: Envelope format support, clean input mapping
3
4
  */
4
- import type { Provider, CognitiveModule, ModuleResult } from '../types.js';
5
+ import type { Provider, CognitiveModule, ModuleResult, ModuleInput } from '../types.js';
5
6
  export interface RunOptions {
7
+ input?: ModuleInput;
6
8
  args?: string;
7
- input?: Record<string, unknown>;
8
- validateInput?: boolean;
9
- validateOutput?: boolean;
10
9
  verbose?: boolean;
10
+ useEnvelope?: boolean;
11
11
  }
12
12
  export declare function runModule(module: CognitiveModule, provider: Provider, options?: RunOptions): Promise<ModuleResult>;
@@ -1,43 +1,86 @@
1
1
  /**
2
2
  * Module Runner - Execute Cognitive Modules
3
+ * v2.1: Envelope format support, clean input mapping
3
4
  */
4
5
  export async function runModule(module, provider, options = {}) {
5
- const { args, input, verbose = false } = options;
6
- // Build input data
7
- let inputData = input || {};
8
- if (args) {
9
- inputData = { $ARGUMENTS: args, query: args };
6
+ const { args, input, verbose = false, useEnvelope } = options;
7
+ // Determine if we should use envelope format
8
+ const shouldUseEnvelope = useEnvelope ?? (module.output?.envelope === true || module.format === 'v2');
9
+ // Build clean input data (v2 style: no $ARGUMENTS pollution)
10
+ const inputData = input || {};
11
+ // Map legacy --args to clean input
12
+ if (args && !inputData.code && !inputData.query) {
13
+ // Determine if args looks like code or natural language
14
+ if (looksLikeCode(args)) {
15
+ inputData.code = args;
16
+ }
17
+ else {
18
+ inputData.query = args;
19
+ }
10
20
  }
11
- // Build prompt
21
+ // Build prompt with clean substitution
12
22
  const prompt = buildPrompt(module, inputData);
13
23
  if (verbose) {
24
+ console.error('--- Module ---');
25
+ console.error(`Name: ${module.name} (${module.format})`);
26
+ console.error(`Responsibility: ${module.responsibility}`);
27
+ console.error(`Envelope: ${shouldUseEnvelope}`);
28
+ console.error('--- Input ---');
29
+ console.error(JSON.stringify(inputData, null, 2));
14
30
  console.error('--- Prompt ---');
15
31
  console.error(prompt);
16
- console.error('--- End Prompt ---');
32
+ console.error('--- End ---');
33
+ }
34
+ // Build system message based on module config
35
+ const systemParts = [
36
+ `You are executing the "${module.name}" Cognitive Module.`,
37
+ '',
38
+ `RESPONSIBILITY: ${module.responsibility}`,
39
+ ];
40
+ if (module.excludes.length > 0) {
41
+ systemParts.push('', 'YOU MUST NOT:');
42
+ module.excludes.forEach(e => systemParts.push(`- ${e}`));
43
+ }
44
+ if (module.constraints) {
45
+ systemParts.push('', 'CONSTRAINTS:');
46
+ if (module.constraints.no_network)
47
+ systemParts.push('- No network access');
48
+ if (module.constraints.no_side_effects)
49
+ systemParts.push('- No side effects');
50
+ if (module.constraints.no_file_write)
51
+ systemParts.push('- No file writes');
52
+ if (module.constraints.no_inventing_data)
53
+ systemParts.push('- Do not invent data');
54
+ }
55
+ if (module.output?.require_behavior_equivalence) {
56
+ systemParts.push('', 'BEHAVIOR EQUIVALENCE:');
57
+ systemParts.push('- You MUST set behavior_equivalence=true ONLY if the output is functionally identical');
58
+ systemParts.push('- If unsure, set behavior_equivalence=false and explain in rationale');
59
+ const maxConfidence = module.constraints?.behavior_equivalence_false_max_confidence ?? 0.7;
60
+ systemParts.push(`- If behavior_equivalence=false, confidence MUST be <= ${maxConfidence}`);
61
+ }
62
+ // Add envelope format instructions
63
+ if (shouldUseEnvelope) {
64
+ systemParts.push('', 'RESPONSE FORMAT (Envelope):');
65
+ systemParts.push('- Wrap your response in the envelope format');
66
+ systemParts.push('- Success: { "ok": true, "data": { ...your output... } }');
67
+ systemParts.push('- Error: { "ok": false, "error": { "code": "ERROR_CODE", "message": "..." } }');
68
+ systemParts.push('- Include "confidence" (0-1) and "rationale" in data');
69
+ if (module.output?.require_behavior_equivalence) {
70
+ systemParts.push('- Include "behavior_equivalence" (boolean) in data');
71
+ }
72
+ }
73
+ else {
74
+ systemParts.push('', 'OUTPUT FORMAT:');
75
+ systemParts.push('- Respond with ONLY valid JSON');
76
+ systemParts.push('- Include "confidence" (0-1) and "rationale" fields');
77
+ if (module.output?.require_behavior_equivalence) {
78
+ systemParts.push('- Include "behavior_equivalence" (boolean) field');
79
+ }
17
80
  }
18
- // Build messages
19
81
  const messages = [
20
- {
21
- role: 'system',
22
- content: `You are executing the "${module.name}" Cognitive Module.
23
-
24
- RESPONSIBILITY: ${module.responsibility}
25
-
26
- YOU MUST NOT:
27
- ${module.excludes.map(e => `- ${e}`).join('\n')}
28
-
29
- REQUIRED OUTPUT FORMAT:
30
- You MUST respond with a valid JSON object. Include these fields:
31
- - All fields required by the output schema
32
- - "confidence": a number between 0 and 1
33
- - "rationale": a string explaining your reasoning
34
-
35
- Respond with ONLY valid JSON, no markdown code blocks.`,
36
- },
37
- {
38
- role: 'user',
39
- content: prompt,
40
- },
82
+ { role: 'system', content: systemParts.join('\n') },
83
+ { role: 'user', content: prompt },
41
84
  ];
42
85
  // Invoke provider
43
86
  const result = await provider.invoke({
@@ -51,43 +94,136 @@ Respond with ONLY valid JSON, no markdown code blocks.`,
51
94
  console.error('--- End Response ---');
52
95
  }
53
96
  // Parse response
54
- let output;
97
+ let parsed;
55
98
  try {
56
- // Try to extract JSON from markdown code blocks
57
99
  const jsonMatch = result.content.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
58
100
  const jsonStr = jsonMatch ? jsonMatch[1] : result.content;
59
- output = JSON.parse(jsonStr.trim());
101
+ parsed = JSON.parse(jsonStr.trim());
60
102
  }
61
103
  catch {
62
104
  throw new Error(`Failed to parse JSON response: ${result.content.substring(0, 500)}`);
63
105
  }
64
- // Extract confidence and rationale
106
+ // Handle envelope format
107
+ if (shouldUseEnvelope && isEnvelopeResponse(parsed)) {
108
+ return parseEnvelopeResponse(parsed, result.content);
109
+ }
110
+ // Handle legacy format (non-envelope)
111
+ return parseLegacyResponse(parsed, result.content);
112
+ }
113
+ /**
114
+ * Check if response is in envelope format
115
+ */
116
+ function isEnvelopeResponse(obj) {
117
+ if (typeof obj !== 'object' || obj === null)
118
+ return false;
119
+ const o = obj;
120
+ return typeof o.ok === 'boolean';
121
+ }
122
+ /**
123
+ * Parse envelope format response
124
+ */
125
+ function parseEnvelopeResponse(response, raw) {
126
+ if (response.ok) {
127
+ const data = response.data;
128
+ return {
129
+ ok: true,
130
+ data: {
131
+ ...data,
132
+ confidence: typeof data.confidence === 'number' ? data.confidence : 0.5,
133
+ rationale: typeof data.rationale === 'string' ? data.rationale : '',
134
+ behavior_equivalence: data.behavior_equivalence,
135
+ },
136
+ raw,
137
+ };
138
+ }
139
+ else {
140
+ return {
141
+ ok: false,
142
+ error: response.error,
143
+ partial_data: response.partial_data,
144
+ raw,
145
+ };
146
+ }
147
+ }
148
+ /**
149
+ * Parse legacy (non-envelope) format response
150
+ */
151
+ function parseLegacyResponse(output, raw) {
65
152
  const outputObj = output;
66
153
  const confidence = typeof outputObj.confidence === 'number' ? outputObj.confidence : 0.5;
67
154
  const rationale = typeof outputObj.rationale === 'string' ? outputObj.rationale : '';
155
+ const behaviorEquivalence = typeof outputObj.behavior_equivalence === 'boolean'
156
+ ? outputObj.behavior_equivalence
157
+ : undefined;
158
+ // Check if this is an error response (has error.code)
159
+ if (outputObj.error && typeof outputObj.error === 'object') {
160
+ const errorObj = outputObj.error;
161
+ if (typeof errorObj.code === 'string') {
162
+ return {
163
+ ok: false,
164
+ error: {
165
+ code: errorObj.code,
166
+ message: typeof errorObj.message === 'string' ? errorObj.message : 'Unknown error',
167
+ },
168
+ raw,
169
+ };
170
+ }
171
+ }
68
172
  return {
69
- output,
70
- confidence,
71
- rationale,
72
- raw: result.content,
173
+ ok: true,
174
+ data: {
175
+ ...outputObj,
176
+ confidence,
177
+ rationale,
178
+ behavior_equivalence: behaviorEquivalence,
179
+ },
180
+ raw,
73
181
  };
74
182
  }
75
- function buildPrompt(module, inputData) {
183
+ /**
184
+ * Build prompt with clean variable substitution
185
+ */
186
+ function buildPrompt(module, input) {
76
187
  let prompt = module.prompt;
77
- // Substitute $ARGUMENTS
78
- const argsValue = String(inputData.$ARGUMENTS || inputData.query || '');
188
+ // v2 style: substitute ${variable} placeholders
189
+ for (const [key, value] of Object.entries(input)) {
190
+ const strValue = typeof value === 'string' ? value : JSON.stringify(value);
191
+ prompt = prompt.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), strValue);
192
+ }
193
+ // v1 compatibility: substitute $ARGUMENTS
194
+ const argsValue = input.code || input.query || '';
79
195
  prompt = prompt.replace(/\$ARGUMENTS/g, argsValue);
80
- // Substitute $N placeholders
81
- const argsList = argsValue.split(/\s+/);
82
- argsList.forEach((arg, i) => {
83
- prompt = prompt.replace(new RegExp(`\\$${i}`, 'g'), arg);
84
- prompt = prompt.replace(new RegExp(`\\$ARGUMENTS\\[${i}\\]`, 'g'), arg);
85
- });
86
- // Substitute other input fields
87
- for (const [key, value] of Object.entries(inputData)) {
88
- if (key !== '$ARGUMENTS' && key !== 'query') {
89
- prompt = prompt.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), String(value));
196
+ // Substitute $N placeholders (v1 compatibility)
197
+ if (typeof argsValue === 'string') {
198
+ const argsList = argsValue.split(/\s+/);
199
+ argsList.forEach((arg, i) => {
200
+ prompt = prompt.replace(new RegExp(`\\$${i}\\b`, 'g'), arg);
201
+ });
202
+ }
203
+ // Append input summary if not already in prompt
204
+ if (!prompt.includes(argsValue) && argsValue) {
205
+ prompt += '\n\n## Input\n\n';
206
+ if (input.code) {
207
+ prompt += '```\n' + input.code + '\n```\n';
208
+ }
209
+ if (input.query) {
210
+ prompt += input.query + '\n';
211
+ }
212
+ if (input.language) {
213
+ prompt += `\nLanguage: ${input.language}\n`;
90
214
  }
91
215
  }
92
216
  return prompt;
93
217
  }
218
+ /**
219
+ * Heuristic to detect if input looks like code
220
+ */
221
+ function looksLikeCode(str) {
222
+ const codeIndicators = [
223
+ /^(def|function|class|const|let|var|import|export|public|private)\s/,
224
+ /[{};()]/,
225
+ /=>/,
226
+ /\.(py|js|ts|go|rs|java|cpp|c|rb)$/,
227
+ ];
228
+ return codeIndicators.some(re => re.test(str));
229
+ }