@koi-language/koi 1.0.5 → 1.1.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 +4 -125
- package/examples/.build/agent-dialogue.ts +138 -0
- package/examples/.build/agent-dialogue.ts.map +1 -0
- package/examples/.build/chess.ts +77 -0
- package/examples/.build/chess.ts.map +1 -0
- package/examples/.build/delegation-test.ts +140 -0
- package/examples/.build/delegation-test.ts.map +1 -0
- package/examples/.build/dialog-demo.ts +77 -0
- package/examples/.build/dialog-demo.ts.map +1 -0
- package/examples/.build/hello-world.ts +77 -0
- package/examples/.build/hello-world.ts.map +1 -0
- package/examples/.build/lover-dialog-demo.ts +77 -0
- package/examples/.build/lover-dialog-demo.ts.map +1 -0
- package/examples/.build/package.json +3 -0
- package/examples/.build/registry-interactive-demo.ts +202 -0
- package/examples/.build/registry-interactive-demo.ts.map +1 -0
- package/examples/.build/registry-playbook-demo.ts +201 -0
- package/examples/.build/registry-playbook-demo.ts.map +1 -0
- package/examples/.build/tic-tac-toe.ts +77 -0
- package/examples/.build/tic-tac-toe.ts.map +1 -0
- package/examples/actions-demo.koi +8 -9
- package/examples/activists-dialogue.koi +75 -0
- package/examples/agent-dialogue.koi +66 -0
- package/examples/chess.koi +19 -0
- package/examples/counter.koi +20 -69
- package/examples/delegation-test.koi +16 -18
- package/examples/dialog-demo.koi +20 -0
- package/examples/hello-world.koi +7 -43
- package/examples/mcp-stdio-demo.koi +29 -0
- package/examples/memory-test.koi +49 -0
- package/examples/mobile-mcp-demo.koi +32 -0
- package/examples/multi-event-handler-test.koi +16 -18
- package/examples/pipeline.koi +15 -17
- package/examples/prompt-demo.koi +20 -0
- package/examples/{registry-playbook-email-compositor.koi → registry-interactive-demo.koi} +27 -27
- package/examples/registry-playbook-demo.koi +28 -28
- package/examples/skill-import-test.koi +7 -9
- package/examples/skills/.build/math-operations.ts +1656 -0
- package/examples/skills/.build/math-operations.ts.map +1 -0
- package/examples/skills/.build/package.json +3 -0
- package/examples/skills/.build/string-operations.ts +1643 -0
- package/examples/skills/.build/string-operations.ts.map +1 -0
- package/examples/skills/advanced/.build/index.ts +3223 -0
- package/examples/skills/advanced/.build/index.ts.map +1 -0
- package/examples/skills/advanced/.build/package.json +3 -0
- package/examples/skills/advanced/index.koi +3 -5
- package/examples/skills/math-operations.koi +1 -3
- package/examples/skills/string-operations.koi +1 -3
- package/examples/tic-tac-toe.koi +19 -0
- package/examples/utils/echo-mcp-server.js +141 -0
- package/examples/web-delegation-demo.koi +15 -17
- package/package.json +2 -1
- package/src/cli/koi.js +30 -41
- package/src/compiler/build-optimizer.js +204 -289
- package/src/compiler/cache-manager.js +1 -1
- package/src/compiler/import-resolver.js +5 -9
- package/src/compiler/parser.js +6072 -3476
- package/src/compiler/transpiler.js +346 -38
- package/src/grammar/koi.pegjs +302 -62
- package/src/runtime/actions/{format.js → call-llm.js} +37 -44
- package/src/runtime/actions/call-mcp.js +97 -0
- package/src/runtime/actions/if.js +179 -0
- package/src/runtime/actions/print.js +3 -1
- package/src/runtime/actions/prompt-user.js +75 -0
- package/src/runtime/actions/repeat.js +147 -0
- package/src/runtime/actions/shell.js +185 -0
- package/src/runtime/actions/while.js +205 -0
- package/src/runtime/agent.js +592 -178
- package/src/runtime/cli-display.js +26 -0
- package/src/runtime/cli-input.js +421 -0
- package/src/runtime/cli-logger.js +2 -5
- package/src/runtime/cli-markdown.js +61 -0
- package/src/runtime/cli-select.js +106 -0
- package/src/runtime/incremental-json-parser.js +51 -17
- package/src/runtime/index.js +1 -0
- package/src/runtime/llm-provider.js +1083 -572
- package/src/runtime/mcp-registry.js +141 -0
- package/src/runtime/mcp-stdio-client.js +334 -0
- package/src/runtime/planner.js +1 -1
- package/src/runtime/playbook-session.js +259 -0
- package/src/runtime/registry-backends/keyv-sqlite.js +1 -1
- package/src/runtime/registry-backends/local.js +1 -1
- package/src/runtime/router.js +22 -26
- package/src/runtime/runtime.js +7 -1
- package/examples/cache-test.koi +0 -29
- package/examples/calculator.koi +0 -61
- package/examples/clear-registry.js +0 -33
- package/examples/clear-registry.koi +0 -30
- package/examples/code-introspection-test.koi +0 -149
- package/examples/directory-import-test.koi +0 -84
- package/examples/hello-world-claude.koi +0 -52
- package/examples/hello.koi +0 -24
- package/examples/mcp-example.koi +0 -70
- package/examples/new-import-test.koi +0 -89
- package/examples/registry-demo.koi +0 -184
- package/examples/registry-playbook-email-compositor-2.koi +0 -140
- package/examples/sentiment.koi +0 -90
- package/examples/simple.koi +0 -48
- package/examples/task-chaining-demo.koi +0 -244
- package/examples/test-await.koi +0 -22
- package/examples/test-crypto-sha256.koi +0 -196
- package/examples/test-delegation.koi +0 -41
- package/examples/test-multi-team-routing.koi +0 -258
- package/examples/test-no-handler.koi +0 -35
- package/examples/test-npm-import.koi +0 -67
- package/examples/test-parse.koi +0 -10
- package/examples/test-peers-with-team.koi +0 -59
- package/examples/test-permissions-fail.koi +0 -20
- package/examples/test-permissions.koi +0 -36
- package/examples/test-simple-registry.koi +0 -31
- package/examples/test-typescript-import.koi +0 -64
- package/examples/test-uses-team-syntax.koi +0 -25
- package/examples/test-uses-team.koi +0 -31
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PlaybookSession - Tracks state for the reactive agentic loop.
|
|
3
|
+
*
|
|
4
|
+
* In reactive mode, the LLM decides ONE action per iteration,
|
|
5
|
+
* receives feedback from the result, and adapts its strategy.
|
|
6
|
+
* This class maintains the state across iterations.
|
|
7
|
+
*
|
|
8
|
+
* The loop runs indefinitely until the agent decides to terminate
|
|
9
|
+
* via a "return" action, or consecutive errors exceed the threshold.
|
|
10
|
+
*/
|
|
11
|
+
export class PlaybookSession {
|
|
12
|
+
constructor({ playbook, agentName } = {}) {
|
|
13
|
+
this.playbook = playbook;
|
|
14
|
+
this.agentName = agentName;
|
|
15
|
+
|
|
16
|
+
// Iteration tracking
|
|
17
|
+
this.iteration = 0;
|
|
18
|
+
this.isTerminated = false;
|
|
19
|
+
this.finalResult = null;
|
|
20
|
+
|
|
21
|
+
// Action history: { action, result, error, timestamp, iteration }
|
|
22
|
+
this.actionHistory = [];
|
|
23
|
+
|
|
24
|
+
// Action context: accumulates results for template variables (${a1.output.field})
|
|
25
|
+
// Same structure as actionContext in agent.js
|
|
26
|
+
this.actionContext = {
|
|
27
|
+
state: {},
|
|
28
|
+
results: [],
|
|
29
|
+
args: {}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Error tracking
|
|
33
|
+
this.lastError = null;
|
|
34
|
+
this.consecutiveErrors = 0;
|
|
35
|
+
this.maxConsecutiveErrors = 10;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if the loop can continue.
|
|
40
|
+
* The loop only stops when the agent terminates or consecutive errors exceed threshold.
|
|
41
|
+
*/
|
|
42
|
+
canContinue() {
|
|
43
|
+
return (
|
|
44
|
+
!this.isTerminated &&
|
|
45
|
+
this.consecutiveErrors < this.maxConsecutiveErrors
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Record an action and its result/error.
|
|
51
|
+
* Updates actionContext with the same chaining logic as agent.js.
|
|
52
|
+
*/
|
|
53
|
+
recordAction(action, result, error = null) {
|
|
54
|
+
this.iteration++;
|
|
55
|
+
|
|
56
|
+
const entry = {
|
|
57
|
+
action,
|
|
58
|
+
result: result || null,
|
|
59
|
+
error: error ? { message: error.message || String(error) } : null,
|
|
60
|
+
timestamp: Date.now(),
|
|
61
|
+
iteration: this.iteration
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
this.actionHistory.push(entry);
|
|
65
|
+
|
|
66
|
+
if (error) {
|
|
67
|
+
this.lastError = error;
|
|
68
|
+
this.consecutiveErrors++;
|
|
69
|
+
} else {
|
|
70
|
+
this.lastError = null;
|
|
71
|
+
this.consecutiveErrors = 0;
|
|
72
|
+
|
|
73
|
+
// Update actionContext (same logic as agent.js onStreamAction)
|
|
74
|
+
if (result && typeof result === 'object') {
|
|
75
|
+
// Unwrap double-encoded results
|
|
76
|
+
let unwrappedResult = result;
|
|
77
|
+
if (result.result && typeof result.result === 'string' && Object.keys(result).length === 1) {
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(result.result);
|
|
80
|
+
if (typeof parsed === 'object') {
|
|
81
|
+
unwrappedResult = parsed;
|
|
82
|
+
}
|
|
83
|
+
} catch (e) {
|
|
84
|
+
// Not JSON, keep as-is
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const resultForContext = JSON.parse(JSON.stringify(unwrappedResult));
|
|
89
|
+
this.actionContext.results.push(resultForContext);
|
|
90
|
+
|
|
91
|
+
// Store result with action ID for explicit referencing
|
|
92
|
+
if (action.id) {
|
|
93
|
+
this.actionContext[action.id] = { output: resultForContext };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Only update previousResult for actions that produce meaningful data
|
|
97
|
+
const intent = action.intent || action.type;
|
|
98
|
+
const nonDataActions = ['print', 'log', 'format'];
|
|
99
|
+
|
|
100
|
+
if (!nonDataActions.includes(intent)) {
|
|
101
|
+
this.actionContext.previousResult = resultForContext;
|
|
102
|
+
this.actionContext.lastResult = resultForContext;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Make result fields directly accessible
|
|
106
|
+
Object.keys(resultForContext).forEach(key => {
|
|
107
|
+
if (!this.actionContext[key]) {
|
|
108
|
+
this.actionContext[key] = resultForContext[key];
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Terminate the loop with a final result
|
|
117
|
+
*/
|
|
118
|
+
terminate(result) {
|
|
119
|
+
this.isTerminated = true;
|
|
120
|
+
this.finalResult = result;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Build feedback context for the next LLM iteration.
|
|
125
|
+
* Kept MINIMAL to avoid context bloat — the LLM already has
|
|
126
|
+
* the full conversation history with all previous actions/results.
|
|
127
|
+
*/
|
|
128
|
+
buildFeedbackContext() {
|
|
129
|
+
const parts = [];
|
|
130
|
+
|
|
131
|
+
// Error feedback
|
|
132
|
+
if (this.lastError) {
|
|
133
|
+
const errorMsg = this.lastError.message || String(this.lastError);
|
|
134
|
+
parts.push(`\u274c LAST ACTION FAILED: ${errorMsg}`);
|
|
135
|
+
|
|
136
|
+
// Count recent MCP errors to suggest diagnostics
|
|
137
|
+
const recentMcpErrors = this.actionHistory.slice(-5).filter(
|
|
138
|
+
e => e.error && (e.action.intent === 'call_mcp' || e.action.type === 'call_mcp')
|
|
139
|
+
).length;
|
|
140
|
+
|
|
141
|
+
if (recentMcpErrors >= 2) {
|
|
142
|
+
parts.push('Multiple MCP failures detected. DIAGNOSE: check available tools on that MCP server for status/diagnostic tools and use them to understand the current state before retrying. The MCP server may have been restarted automatically.');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
parts.push('NEVER give up. Try a DIFFERENT approach — do NOT repeat the same action that failed. Think about what went wrong and find an alternative path to achieve the goal.');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Loop detection warnings
|
|
149
|
+
const warnings = this._detectLoops();
|
|
150
|
+
for (const warning of warnings) {
|
|
151
|
+
parts.push(`\u26a0\ufe0f ${warning}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Last action result
|
|
155
|
+
const lastEntry = this.actionHistory[this.actionHistory.length - 1];
|
|
156
|
+
if (lastEntry && !lastEntry.error) {
|
|
157
|
+
const intent = lastEntry.action.intent || lastEntry.action.type || 'unknown';
|
|
158
|
+
const id = lastEntry.action.id ? ` [${lastEntry.action.id}]` : '';
|
|
159
|
+
const resultStr = lastEntry.result
|
|
160
|
+
? JSON.stringify(lastEntry.result)
|
|
161
|
+
: 'ok';
|
|
162
|
+
|
|
163
|
+
// Detect when result payload indicates failure (e.g. MCP returns {success: false, error: "..."})
|
|
164
|
+
if (lastEntry.result && lastEntry.result.success === false && lastEntry.result.error) {
|
|
165
|
+
const errorMsg = lastEntry.result.error;
|
|
166
|
+
parts.push(`\u274c${id} ${intent} returned an error: ${errorMsg}`);
|
|
167
|
+
|
|
168
|
+
// Include MCP server output (stderr) if available — this often contains
|
|
169
|
+
// the actual installation commands or detailed error info
|
|
170
|
+
if (lastEntry.result.serverOutput) {
|
|
171
|
+
parts.push(`Server output:\n${lastEntry.result.serverOutput}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check if server output contains actual installation commands
|
|
175
|
+
const serverOutput = lastEntry.result.serverOutput || '';
|
|
176
|
+
const hasInstallCommands = /\b(brew\s+install|pip\s+install|npm\s+install|apt\s+install|apt-get\s+install)\b/i.test(serverOutput);
|
|
177
|
+
|
|
178
|
+
// Check if this SAME error appeared before in history (fix attempt didn't work)
|
|
179
|
+
const sameErrorBefore = this.actionHistory.slice(0, -1).some(
|
|
180
|
+
e => e.result && e.result.success === false && e.result.error === errorMsg
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
if (sameErrorBefore) {
|
|
184
|
+
parts.push('WARNING: This is the SAME error as before — your previous fix did NOT solve it. If you have already tried everything mentioned, inform the user and return with an error.');
|
|
185
|
+
} else if (hasInstallCommands) {
|
|
186
|
+
parts.push('APPLY RULE 8: Read the server output above. Use "shell" to run the EXACT commands listed there — ALL of them in a SINGLE shell command chained with && (e.g. "brew tap X && brew install Y && pip install Z"). IMPORTANT: The shell "description" field must express NEED, not action (e.g. "Need to install X, Y, and Z because the MCP server requires IDB for iOS Simulator control" — NOT "Installing X..."). Then RETRY the failed action.');
|
|
187
|
+
parts.push('SHELL RULES: NEVER use placeholder values like <your_api_key> or <TOKEN> in commands — they cause syntax errors. NEVER try to set/export API keys — they are already in the environment. ONLY install what the server output explicitly lists.');
|
|
188
|
+
} else {
|
|
189
|
+
parts.push('This is a configuration or environment error (e.g. missing API key, wrong path). Do NOT try to install anything. Inform the user what is wrong and return with an error explaining how to fix it.');
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
parts.push(`\u2705${id} ${intent} -> ${resultStr}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return parts.join('\n');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Detect loop patterns in action history
|
|
201
|
+
*/
|
|
202
|
+
_detectLoops() {
|
|
203
|
+
const warnings = [];
|
|
204
|
+
const history = this.actionHistory;
|
|
205
|
+
|
|
206
|
+
if (history.length < 2) return warnings;
|
|
207
|
+
|
|
208
|
+
// Same action repeated 2x consecutively — compare full action shape
|
|
209
|
+
// (not just intent+data, since call_mcp actions differ by tool/input, not data)
|
|
210
|
+
const last = history[history.length - 1];
|
|
211
|
+
const prev = history[history.length - 2];
|
|
212
|
+
const lastKey = JSON.stringify({ intent: last.action.intent, tool: last.action.tool, data: last.action.data, input: last.action.input });
|
|
213
|
+
const prevKey = JSON.stringify({ intent: prev.action.intent, tool: prev.action.tool, data: prev.action.data, input: prev.action.input });
|
|
214
|
+
|
|
215
|
+
if (lastKey === prevKey) {
|
|
216
|
+
warnings.push('You repeated the same action with the same data. Try a different approach!');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Consecutive errors
|
|
220
|
+
if (this.consecutiveErrors >= 2) {
|
|
221
|
+
warnings.push(`${this.consecutiveErrors} consecutive errors. Try a completely different approach!`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Oscillating pattern A-B-A-B (compare full action shape, not just intent)
|
|
225
|
+
if (history.length >= 4) {
|
|
226
|
+
const h = history.slice(-4);
|
|
227
|
+
const keys = h.map(e => JSON.stringify({
|
|
228
|
+
intent: e.action.intent, tool: e.action.tool,
|
|
229
|
+
data: e.action.data, input: e.action.input, command: e.action.command
|
|
230
|
+
}));
|
|
231
|
+
if (keys[0] === keys[2] && keys[1] === keys[3] && keys[0] !== keys[1]) {
|
|
232
|
+
const intent0 = h[0].action.tool || h[0].action.intent || h[0].action.type;
|
|
233
|
+
const intent1 = h[1].action.tool || h[1].action.intent || h[1].action.type;
|
|
234
|
+
warnings.push(`Oscillating pattern detected (${intent0} <-> ${intent1}). Break the cycle!`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return warnings;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Get available data references (action IDs with output)
|
|
243
|
+
*/
|
|
244
|
+
_getAvailableDataRefs() {
|
|
245
|
+
const refs = [];
|
|
246
|
+
|
|
247
|
+
for (const key of Object.keys(this.actionContext)) {
|
|
248
|
+
if (key.match(/^a\d+$/) || (this.actionContext[key]?.output !== undefined && !['state', 'results', 'args', 'previousResult', 'lastResult'].includes(key))) {
|
|
249
|
+
const output = this.actionContext[key]?.output;
|
|
250
|
+
if (output !== undefined) {
|
|
251
|
+
const preview = JSON.stringify(output).substring(0, 80);
|
|
252
|
+
refs.push(`\${${key}.output} = ${preview}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return refs;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -12,7 +12,7 @@ import path from 'path';
|
|
|
12
12
|
|
|
13
13
|
export default class KeyvSqliteBackend {
|
|
14
14
|
constructor(options = {}) {
|
|
15
|
-
this.dbPath = options.path || '.koi
|
|
15
|
+
this.dbPath = options.path || '.koi/registry/registry.sqlite';
|
|
16
16
|
this.keyv = null;
|
|
17
17
|
this.namespace = options.namespace || 'koi';
|
|
18
18
|
this._keysCache = new Set();
|
|
@@ -10,7 +10,7 @@ import path from 'path';
|
|
|
10
10
|
|
|
11
11
|
export default class LocalBackend {
|
|
12
12
|
constructor(options = {}) {
|
|
13
|
-
this.dataDir = options.path || '.koi
|
|
13
|
+
this.dataDir = options.path || '.koi/registry';
|
|
14
14
|
this.dataFile = path.join(this.dataDir, 'data.json');
|
|
15
15
|
this.cache = new Map();
|
|
16
16
|
this.dirty = false;
|
package/src/runtime/router.js
CHANGED
|
@@ -39,49 +39,33 @@ export class AgentRouter {
|
|
|
39
39
|
|
|
40
40
|
this.agents.set(agent.name, agent);
|
|
41
41
|
|
|
42
|
-
//
|
|
42
|
+
// Store affordances without generating embeddings (lazy generation)
|
|
43
43
|
if (cachedAffordances) {
|
|
44
44
|
for (const [eventName, aff] of Object.entries(cachedAffordances)) {
|
|
45
|
-
if (!aff.
|
|
46
|
-
// Fallback: generate at runtime if cache is incomplete
|
|
47
|
-
if (aff.description && aff.description.trim() !== '') {
|
|
48
|
-
aff.embedding = await this.getEmbedding(aff.description);
|
|
49
|
-
} else {
|
|
50
|
-
console.warn(`⚠️ [Router] Skipping ${agent.name}.${eventName} - empty description`);
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
45
|
+
if (!aff.description || aff.description.trim() === '') continue;
|
|
54
46
|
|
|
55
47
|
this.affordanceEmbeddings.push({
|
|
56
48
|
agent: agent,
|
|
57
49
|
event: eventName,
|
|
58
50
|
description: aff.description,
|
|
59
|
-
embedding: aff.embedding,
|
|
51
|
+
embedding: aff.embedding || null, // May be null — generated lazily
|
|
60
52
|
confidence: aff.confidence,
|
|
61
53
|
metadata: { hasPlaybook: aff.hasPlaybook }
|
|
62
54
|
});
|
|
63
55
|
}
|
|
64
|
-
|
|
65
56
|
return;
|
|
66
57
|
}
|
|
67
58
|
|
|
68
|
-
// No cache: extract
|
|
59
|
+
// No cache: extract affordances (still no embedding generation)
|
|
69
60
|
const affordances = this.extractAffordances(agent);
|
|
70
|
-
|
|
71
|
-
// Generate embeddings for each affordance
|
|
72
61
|
for (const aff of affordances) {
|
|
73
|
-
if (!aff.description || aff.description.trim() === '')
|
|
74
|
-
console.warn(`⚠️ [Router] Skipping ${agent.name}.${aff.event} - empty description`);
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const embedding = await this.getEmbedding(aff.description);
|
|
62
|
+
if (!aff.description || aff.description.trim() === '') continue;
|
|
79
63
|
|
|
80
64
|
this.affordanceEmbeddings.push({
|
|
81
65
|
agent: agent,
|
|
82
66
|
event: aff.event,
|
|
83
67
|
description: aff.description,
|
|
84
|
-
embedding:
|
|
68
|
+
embedding: null, // Generated lazily on first findMatches call
|
|
85
69
|
confidence: aff.confidence,
|
|
86
70
|
metadata: aff.metadata
|
|
87
71
|
});
|
|
@@ -206,13 +190,25 @@ export class AgentRouter {
|
|
|
206
190
|
return [];
|
|
207
191
|
}
|
|
208
192
|
|
|
193
|
+
// Lazy embedding generation: only compute when actually needed for routing
|
|
194
|
+
const needsEmbeddings = this.affordanceEmbeddings.some(aff => !aff.embedding);
|
|
195
|
+
if (needsEmbeddings) {
|
|
196
|
+
for (const aff of this.affordanceEmbeddings) {
|
|
197
|
+
if (!aff.embedding && aff.description) {
|
|
198
|
+
aff.embedding = await this.getEmbedding(aff.description);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
209
203
|
// Phase 1: Embedding-based similarity search
|
|
210
204
|
const intentEmbedding = await this.getEmbedding(intent);
|
|
211
205
|
|
|
212
|
-
const similarities = this.affordanceEmbeddings
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
206
|
+
const similarities = this.affordanceEmbeddings
|
|
207
|
+
.filter(aff => aff.embedding)
|
|
208
|
+
.map(aff => ({
|
|
209
|
+
...aff,
|
|
210
|
+
similarity: this.cosineSimilarity(intentEmbedding, aff.embedding)
|
|
211
|
+
}));
|
|
216
212
|
|
|
217
213
|
// Sort by similarity descending
|
|
218
214
|
similarities.sort((a, b) => b.similarity - a.similarity);
|
package/src/runtime/runtime.js
CHANGED
|
@@ -3,7 +3,7 @@ import { cliLogger } from './cli-logger.js';
|
|
|
3
3
|
|
|
4
4
|
export class Runtime {
|
|
5
5
|
static async send(config) {
|
|
6
|
-
const { base, filters = [], args = {}, timeout = 30000 } = config;
|
|
6
|
+
const { base, filters = [], args = {}, timeout = 30000, caller = null } = config;
|
|
7
7
|
|
|
8
8
|
// Validate that base (team/peers) is not null
|
|
9
9
|
if (!base || base === null || base === undefined) {
|
|
@@ -11,6 +11,12 @@ export class Runtime {
|
|
|
11
11
|
throw new Error(`NO_AGENT_HANDLER:${eventName}::no-team`);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
// Check that calling agent has delegate permission
|
|
15
|
+
if (caller && typeof caller.hasPermission === 'function' && !caller.hasPermission('delegate')) {
|
|
16
|
+
const eventName = filters.find(f => f.type === 'event')?.name || 'unknown event';
|
|
17
|
+
throw new Error(`Agent "${caller.name}" cannot delegate: role "${caller.role?.name || 'unknown'}" lacks "can delegate" permission. Add "can delegate" to the role definition.`);
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
// Apply timeout
|
|
15
21
|
const timeoutPromise = new Promise((_, reject) => {
|
|
16
22
|
setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout);
|
package/examples/cache-test.koi
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
// Cache Test Example
|
|
2
|
-
package "test.cache"
|
|
3
|
-
|
|
4
|
-
role Worker { can execute }
|
|
5
|
-
|
|
6
|
-
Agent TextTranslator : Worker {
|
|
7
|
-
llm default = { provider: "openai", model: "gpt-4o-mini" }
|
|
8
|
-
|
|
9
|
-
on translate(args: Json) {
|
|
10
|
-
playbook """
|
|
11
|
-
Translate this text to the target language: ${args.text}
|
|
12
|
-
Target language: ${args.language}
|
|
13
|
-
Return JSON with translated text.
|
|
14
|
-
"""
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
Agent WordAnalyzer : Worker {
|
|
19
|
-
llm default = { provider: "openai", model: "gpt-4o-mini" }
|
|
20
|
-
|
|
21
|
-
on analyze(args: Json) {
|
|
22
|
-
playbook """
|
|
23
|
-
Analyze this text and count words: ${args.text}
|
|
24
|
-
Return JSON with word count and analysis.
|
|
25
|
-
"""
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
run TextTranslator.translate({ text: "hello", language: "spanish" })
|
package/examples/calculator.koi
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Koi — Calculator Example
|
|
3
|
-
// Simple calculator with multiple operations (procedural code only)
|
|
4
|
-
// ============================================================
|
|
5
|
-
|
|
6
|
-
package "demo.koi.calculator"
|
|
7
|
-
|
|
8
|
-
role Worker { can execute }
|
|
9
|
-
|
|
10
|
-
// All handlers use procedural code (no playbooks)
|
|
11
|
-
Agent Calculator : Worker {
|
|
12
|
-
on add(args: Json) {
|
|
13
|
-
const a = args.a
|
|
14
|
-
const b = args.b
|
|
15
|
-
return { result: a + b, operation: "add" }
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
on multiply(args: Json) {
|
|
19
|
-
const a = args.a
|
|
20
|
-
const b = args.b
|
|
21
|
-
return { result: a * b, operation: "multiply" }
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
on divide(args: Json) {
|
|
25
|
-
const a = args.a
|
|
26
|
-
const b = args.b
|
|
27
|
-
|
|
28
|
-
if b == 0 {
|
|
29
|
-
return { error: "Division by zero", operation: "divide" }
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return { result: a / b, operation: "divide" }
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
Team CalcTeam {
|
|
37
|
-
calc = Calculator
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
Agent Orchestrator : Worker {
|
|
41
|
-
uses Team CalcTeam
|
|
42
|
-
|
|
43
|
-
on start(args: Json) {
|
|
44
|
-
const sum =
|
|
45
|
-
await send peers.event("add").role(Worker).any()({ a: 10, b: 5 }) timeout 2s
|
|
46
|
-
|
|
47
|
-
const product =
|
|
48
|
-
await send peers.event("multiply").role(Worker).any()({ a: 10, b: 5 }) timeout 2s
|
|
49
|
-
|
|
50
|
-
const quotient =
|
|
51
|
-
await send peers.event("divide").role(Worker).any()({ a: 10, b: 5 }) timeout 2s
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
sum: sum,
|
|
55
|
-
product: product,
|
|
56
|
-
quotient: quotient
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
run Orchestrator.start({})
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// Clear Registry - Delete all data from the registry
|
|
4
|
-
import { registry } from '../src/runtime/index.js';
|
|
5
|
-
|
|
6
|
-
async function clearRegistry() {
|
|
7
|
-
console.log("⚠️ WARNING: Clearing all registry data...");
|
|
8
|
-
console.log("");
|
|
9
|
-
|
|
10
|
-
// Show stats before clearing
|
|
11
|
-
const statsBefore = await registry.stats();
|
|
12
|
-
console.log(" Current entries:", statsBefore.count);
|
|
13
|
-
console.log(" Storage:", statsBefore.file);
|
|
14
|
-
console.log("");
|
|
15
|
-
|
|
16
|
-
// Clear the registry
|
|
17
|
-
await registry.clear();
|
|
18
|
-
|
|
19
|
-
console.log("✅ Registry cleared successfully!");
|
|
20
|
-
console.log("");
|
|
21
|
-
|
|
22
|
-
// Show stats after clearing
|
|
23
|
-
const statsAfter = await registry.stats();
|
|
24
|
-
console.log(" Entries remaining:", statsAfter.count);
|
|
25
|
-
console.log("");
|
|
26
|
-
|
|
27
|
-
process.exit(0);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
clearRegistry().catch((error) => {
|
|
31
|
-
console.error("❌ Error clearing registry:", error.message);
|
|
32
|
-
process.exit(1);
|
|
33
|
-
});
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
// Clear Registry - Delete all data from the registry
|
|
2
|
-
package "clear.registry"
|
|
3
|
-
|
|
4
|
-
role Admin { can execute }
|
|
5
|
-
|
|
6
|
-
Agent Cleaner : Admin {
|
|
7
|
-
on clearAll(args: Json) {
|
|
8
|
-
console.log("⚠️ WARNING: Clearing all registry data...")
|
|
9
|
-
|
|
10
|
-
// Show current stats before clearing
|
|
11
|
-
const statsBefore = await registry.stats()
|
|
12
|
-
console.log(" Current entries:", statsBefore.count)
|
|
13
|
-
console.log(" Storage:", statsBefore.file)
|
|
14
|
-
console.log("")
|
|
15
|
-
|
|
16
|
-
// Clear the registry
|
|
17
|
-
await registry.clear()
|
|
18
|
-
|
|
19
|
-
console.log("✅ Registry cleared successfully!")
|
|
20
|
-
|
|
21
|
-
// Show stats after clearing
|
|
22
|
-
const statsAfter = await registry.stats()
|
|
23
|
-
console.log(" Entries remaining:", statsAfter.count)
|
|
24
|
-
console.log("")
|
|
25
|
-
|
|
26
|
-
return { success: true, clearedCount: statsBefore.count }
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
run Cleaner.clearAll({})
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
package "demo.code.introspection"
|
|
2
|
-
|
|
3
|
-
role Calculator { can calculate }
|
|
4
|
-
role Coordinator { can coordinate }
|
|
5
|
-
|
|
6
|
-
// Agent con event handler que tiene código (no playbook)
|
|
7
|
-
// El sistema debe hacer introspección para generar affordance
|
|
8
|
-
Agent MathProcessor : Calculator {
|
|
9
|
-
llm default = {
|
|
10
|
-
provider: "openai",
|
|
11
|
-
model: "gpt-4o-mini",
|
|
12
|
-
temperature: 0.1
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// Event handler con código Koi (sin playbook)
|
|
16
|
-
// El sistema hará introspección del código para generar affordance
|
|
17
|
-
// Este handler suma todos los números en el array
|
|
18
|
-
on calculateSum(args: Json) {
|
|
19
|
-
const numbers = args.numbers
|
|
20
|
-
|
|
21
|
-
// Procesar números y sumarlos
|
|
22
|
-
const result = await send peers.event("sumArray").role(Calculator).any()({
|
|
23
|
-
data: numbers
|
|
24
|
-
}) timeout 5s
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
operation: "sum",
|
|
28
|
-
result: result
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Event handler que multiplica todos los números
|
|
33
|
-
on calculateProduct(args: Json) {
|
|
34
|
-
const numbers = args.numbers
|
|
35
|
-
|
|
36
|
-
// Multiplicar los números usando delegación
|
|
37
|
-
const result = await send peers.event("multiplyArray").role(Calculator).any()({
|
|
38
|
-
data: numbers
|
|
39
|
-
}) timeout 5s
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
operation: "product",
|
|
43
|
-
result: result
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Event handler con playbook (para comparar)
|
|
48
|
-
on calculateAverage(args: Json) {
|
|
49
|
-
playbook """
|
|
50
|
-
Calculate the average of ${JSON.stringify(args.numbers)}
|
|
51
|
-
|
|
52
|
-
Sum all numbers and divide by count.
|
|
53
|
-
Return: { "operation": "average", "result": <value> }
|
|
54
|
-
"""
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
Team MathTeam {
|
|
59
|
-
processor = MathProcessor
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Coordinator que delega basándose en affordances
|
|
63
|
-
Agent Coordinator : Coordinator {
|
|
64
|
-
llm default = {
|
|
65
|
-
provider: "openai",
|
|
66
|
-
model: "gpt-4o-mini",
|
|
67
|
-
temperature: 0.1
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
uses Team MathTeam
|
|
71
|
-
|
|
72
|
-
on process(args: Json) {
|
|
73
|
-
playbook """
|
|
74
|
-
Process this math request: ${JSON.stringify(args)}
|
|
75
|
-
|
|
76
|
-
Available operations (delegate to Calculator role):
|
|
77
|
-
- Calculate sum/total of numbers
|
|
78
|
-
- Calculate product/multiplication of numbers
|
|
79
|
-
- Calculate average/mean of numbers
|
|
80
|
-
|
|
81
|
-
Determine the intent and delegate with an action:
|
|
82
|
-
{
|
|
83
|
-
"actions": [{
|
|
84
|
-
"title": "Calculating...",
|
|
85
|
-
"intent": "<your interpretation of what math operation is needed>",
|
|
86
|
-
"data": { "numbers": ${JSON.stringify(args.numbers)} }
|
|
87
|
-
}]
|
|
88
|
-
}
|
|
89
|
-
"""
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
Team CoordinatorTeam {
|
|
94
|
-
coordinator = Coordinator
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
Agent Main : Coordinator {
|
|
98
|
-
uses Team CoordinatorTeam
|
|
99
|
-
|
|
100
|
-
on start(args: Json) {
|
|
101
|
-
console.log("╔════════════════════════════════════════════╗")
|
|
102
|
-
console.log("║ Code Introspection Test ║")
|
|
103
|
-
console.log("║ Event handlers with source code ║")
|
|
104
|
-
console.log("╚════════════════════════════════════════════╝\n")
|
|
105
|
-
|
|
106
|
-
// Test 1: Request sum (should route to calculateSum)
|
|
107
|
-
console.log("📋 Test 1: Calculate the total")
|
|
108
|
-
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
|
109
|
-
|
|
110
|
-
const sumResult =
|
|
111
|
-
await send peers.event("process").role(Coordinator).any()({
|
|
112
|
-
operation: "calculate the total of these numbers",
|
|
113
|
-
numbers: [5, 10, 15, 20]
|
|
114
|
-
}) timeout 30s
|
|
115
|
-
|
|
116
|
-
console.log("Result:", JSON.stringify(sumResult, null, 2))
|
|
117
|
-
|
|
118
|
-
// Test 2: Request product (should route to calculateProduct)
|
|
119
|
-
console.log("\n📋 Test 2: Multiply all numbers")
|
|
120
|
-
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
|
121
|
-
|
|
122
|
-
const productResult =
|
|
123
|
-
await send peers.event("process").role(Coordinator).any()({
|
|
124
|
-
operation: "multiply all the numbers together",
|
|
125
|
-
numbers: [2, 3, 4, 5]
|
|
126
|
-
}) timeout 30s
|
|
127
|
-
|
|
128
|
-
console.log("Result:", JSON.stringify(productResult, null, 2))
|
|
129
|
-
|
|
130
|
-
// Test 3: Request average (should route to calculateAverage with playbook)
|
|
131
|
-
console.log("\n📋 Test 3: Find the average")
|
|
132
|
-
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
|
133
|
-
|
|
134
|
-
const avgResult =
|
|
135
|
-
await send peers.event("process").role(Coordinator).any()({
|
|
136
|
-
operation: "find the average value",
|
|
137
|
-
numbers: [10, 20, 30, 40, 50]
|
|
138
|
-
}) timeout 30s
|
|
139
|
-
|
|
140
|
-
console.log("Result:", JSON.stringify(avgResult, null, 2))
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
success: true,
|
|
144
|
-
tests_completed: 3
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
run Main.start({})
|