thinyai 0.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/LICENSE +21 -0
- package/README.md +38 -0
- package/dist/bin.js +2277 -0
- package/package.json +71 -0
package/dist/bin.js
ADDED
|
@@ -0,0 +1,2277 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/main.ts
|
|
4
|
+
import { createInterface } from "readline/promises";
|
|
5
|
+
import { clearLine, cursorTo } from "readline";
|
|
6
|
+
import { stdin, stdout } from "process";
|
|
7
|
+
import { z as z4 } from "zod";
|
|
8
|
+
|
|
9
|
+
// ../../packages/core/src/domain/messages.ts
|
|
10
|
+
var systemMessage = (content) => ({ role: "system", content });
|
|
11
|
+
|
|
12
|
+
// ../../packages/core/src/tool.ts
|
|
13
|
+
function defineTool(tool) {
|
|
14
|
+
return tool;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ../../packages/core/src/errors.ts
|
|
18
|
+
var MaxStepsError = class extends Error {
|
|
19
|
+
constructor(steps) {
|
|
20
|
+
super(
|
|
21
|
+
`Agent exceeded the maximum of ${String(steps)} steps without producing a final answer. Increase maxSteps if your workflow needs more, or check for a tool-calling loop.`
|
|
22
|
+
);
|
|
23
|
+
this.steps = steps;
|
|
24
|
+
this.name = "MaxStepsError";
|
|
25
|
+
}
|
|
26
|
+
steps;
|
|
27
|
+
};
|
|
28
|
+
var BudgetError = class extends Error {
|
|
29
|
+
constructor(message, options) {
|
|
30
|
+
super(message, options);
|
|
31
|
+
this.name = "BudgetError";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ../../packages/core/src/events.ts
|
|
36
|
+
var EventBus = class {
|
|
37
|
+
/**
|
|
38
|
+
* @param logger - Optional logger for reporting handler errors.
|
|
39
|
+
* When provided, handler exceptions are logged as structured errors
|
|
40
|
+
* instead of falling back to `console.error`. Pass the agent's session
|
|
41
|
+
* logger for full observability continuity.
|
|
42
|
+
*/
|
|
43
|
+
constructor(logger) {
|
|
44
|
+
this.logger = logger;
|
|
45
|
+
}
|
|
46
|
+
logger;
|
|
47
|
+
handlers = /* @__PURE__ */ new Map();
|
|
48
|
+
/**
|
|
49
|
+
* Subscribe a handler to an event.
|
|
50
|
+
* The same handler instance can be registered only once per event
|
|
51
|
+
* (Set semantics — duplicates are silently ignored).
|
|
52
|
+
*/
|
|
53
|
+
on(event, handler) {
|
|
54
|
+
const set = this.handlers.get(event) ?? /* @__PURE__ */ new Set();
|
|
55
|
+
set.add(handler);
|
|
56
|
+
this.handlers.set(event, set);
|
|
57
|
+
}
|
|
58
|
+
/** Unsubscribe a previously registered handler. No-op if not registered. */
|
|
59
|
+
off(event, handler) {
|
|
60
|
+
this.handlers.get(event)?.delete(handler);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Emit an event to all registered handlers.
|
|
64
|
+
*
|
|
65
|
+
* Handlers are invoked synchronously in registration order.
|
|
66
|
+
* A throwing handler is caught, reported via the configured logger (or
|
|
67
|
+
* `console.error` as a last resort), and the remaining handlers continue.
|
|
68
|
+
*/
|
|
69
|
+
emit(event, payload) {
|
|
70
|
+
for (const handler of this.handlers.get(event) ?? []) {
|
|
71
|
+
try {
|
|
72
|
+
handler(payload);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
75
|
+
const errStack = err instanceof Error ? err.stack : void 0;
|
|
76
|
+
if (this.logger) {
|
|
77
|
+
this.logger.error(
|
|
78
|
+
{
|
|
79
|
+
event: "event_handler_error",
|
|
80
|
+
kernelEvent: event,
|
|
81
|
+
errorMessage: errMsg,
|
|
82
|
+
errorStack: errStack
|
|
83
|
+
},
|
|
84
|
+
`EventBus handler for "${event}" threw: ${errMsg}`
|
|
85
|
+
);
|
|
86
|
+
} else {
|
|
87
|
+
console.error(`[thiny/EventBus] handler for "${event}" threw:`, err);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// ../../packages/core/src/registry.ts
|
|
95
|
+
var ToolRegistry = class {
|
|
96
|
+
map = /* @__PURE__ */ new Map();
|
|
97
|
+
/**
|
|
98
|
+
* Register a tool.
|
|
99
|
+
* Tool names must be unique across all registered tools.
|
|
100
|
+
*
|
|
101
|
+
* @throws {Error} When a tool with the same name is already registered.
|
|
102
|
+
* The error message includes the conflicting tool name and a hint to
|
|
103
|
+
* check which plugin contributed the duplicate.
|
|
104
|
+
*/
|
|
105
|
+
register(tool) {
|
|
106
|
+
if (this.map.has(tool.name)) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Tool already registered: "${tool.name}". Check for duplicate tool names across your plugins.`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
this.map.set(tool.name, tool);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Retrieve a tool by name.
|
|
115
|
+
*
|
|
116
|
+
* @throws {Error} When no tool with the given name is registered.
|
|
117
|
+
* The error includes the total count of registered tools.
|
|
118
|
+
* Tool names are intentionally omitted from the error message to avoid
|
|
119
|
+
* leaking the agent's capability surface in multi-tenant environments.
|
|
120
|
+
* Use `registry.all()` in your own debug tooling to inspect names.
|
|
121
|
+
*/
|
|
122
|
+
get(name) {
|
|
123
|
+
const tool = this.map.get(name);
|
|
124
|
+
if (!tool) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Unknown tool: "${name}". ${String(this.map.size)} tool(s) are registered. Check that the plugin providing this tool was loaded.`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return tool;
|
|
130
|
+
}
|
|
131
|
+
/** Return all registered tools in insertion order. */
|
|
132
|
+
all() {
|
|
133
|
+
return [...this.map.values()];
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// ../../packages/core/src/compose.ts
|
|
138
|
+
function composeModel(middlewares, base) {
|
|
139
|
+
return middlewares.reduceRight((next, mw) => (req) => mw(req, next), base);
|
|
140
|
+
}
|
|
141
|
+
function composeTool(middlewares, base) {
|
|
142
|
+
return middlewares.reduceRight((next, mw) => (call) => mw(call, next), base);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ../../packages/core/src/loop.ts
|
|
146
|
+
async function validateAndRunTool(tool, args, ctx) {
|
|
147
|
+
const parsed = tool.parameters.parse(args);
|
|
148
|
+
const result = await tool.execute(parsed, ctx);
|
|
149
|
+
return JSON.stringify(result ?? null);
|
|
150
|
+
}
|
|
151
|
+
async function executeToolCall(call, runTool, ctx) {
|
|
152
|
+
ctx.events.emit("beforeToolCall", { call });
|
|
153
|
+
let content;
|
|
154
|
+
try {
|
|
155
|
+
content = await runTool(ctx.tools.get(call.name), call.args, ctx);
|
|
156
|
+
} catch (err) {
|
|
157
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
158
|
+
content = `ERROR: ${message}`;
|
|
159
|
+
ctx.events.emit("onError", { call, error: message });
|
|
160
|
+
}
|
|
161
|
+
ctx.events.emit("afterToolCall", { call, content });
|
|
162
|
+
return { role: "tool", toolCallId: call.id, toolName: call.name, content };
|
|
163
|
+
}
|
|
164
|
+
async function runLoop(input, ctx, opts = {}) {
|
|
165
|
+
const generate = opts.generate ?? ctx.model.generate.bind(ctx.model);
|
|
166
|
+
const runTool = opts.runTool ?? validateAndRunTool;
|
|
167
|
+
const messages = [...opts.seed ?? [], { role: "user", content: input }];
|
|
168
|
+
ctx.events.emit("onStart", { sessionId: ctx.sessionId });
|
|
169
|
+
for (let step = 0; step < ctx.maxSteps; step++) {
|
|
170
|
+
ctx.events.emit("beforeModelCall", { step, messages });
|
|
171
|
+
const response = await generate(messages, ctx.tools.all());
|
|
172
|
+
ctx.events.emit("afterModelCall", { step, response });
|
|
173
|
+
messages.push({
|
|
174
|
+
role: "assistant",
|
|
175
|
+
content: response.text ?? "",
|
|
176
|
+
toolCalls: response.toolCalls
|
|
177
|
+
});
|
|
178
|
+
if (!response.toolCalls?.length) {
|
|
179
|
+
const text2 = response.text ?? "";
|
|
180
|
+
ctx.events.emit("onFinish", { step, text: text2 });
|
|
181
|
+
return { text: text2, messages };
|
|
182
|
+
}
|
|
183
|
+
const lockPromises = /* @__PURE__ */ new Map();
|
|
184
|
+
const toolResults = [];
|
|
185
|
+
const promises = response.toolCalls.map(async (call, index) => {
|
|
186
|
+
const tool = ctx.tools.get(call.name);
|
|
187
|
+
const locks = tool.locks ?? [];
|
|
188
|
+
const acquireLocks = async () => {
|
|
189
|
+
for (; ; ) {
|
|
190
|
+
const conflicting = locks.map((lock) => lockPromises.get(lock)).filter((p2) => p2 !== void 0);
|
|
191
|
+
if (conflicting.length === 0) {
|
|
192
|
+
let resolve2;
|
|
193
|
+
const promise = new Promise((r) => {
|
|
194
|
+
resolve2 = r;
|
|
195
|
+
});
|
|
196
|
+
locks.forEach((lock) => lockPromises.set(lock, promise));
|
|
197
|
+
return () => {
|
|
198
|
+
locks.forEach((lock) => {
|
|
199
|
+
if (lockPromises.get(lock) === promise) {
|
|
200
|
+
lockPromises.delete(lock);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
resolve2();
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
await Promise.race(conflicting);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
const releaseLocks = await acquireLocks();
|
|
210
|
+
try {
|
|
211
|
+
const res = await executeToolCall(call, runTool, ctx);
|
|
212
|
+
toolResults[index] = res;
|
|
213
|
+
} finally {
|
|
214
|
+
releaseLocks();
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
await Promise.all(promises);
|
|
218
|
+
messages.push(...toolResults);
|
|
219
|
+
}
|
|
220
|
+
throw new MaxStepsError(ctx.maxSteps);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ../../packages/core/src/stream.ts
|
|
224
|
+
async function assembleStream(stream, onText) {
|
|
225
|
+
let text2 = "";
|
|
226
|
+
const toolCalls = [];
|
|
227
|
+
let finishReason = "stop";
|
|
228
|
+
let usage;
|
|
229
|
+
for await (const event of stream) {
|
|
230
|
+
if (event.type === "text-delta") {
|
|
231
|
+
text2 += event.text;
|
|
232
|
+
onText?.(event.text);
|
|
233
|
+
} else if (event.type === "tool-call") {
|
|
234
|
+
toolCalls.push(event.toolCall);
|
|
235
|
+
} else {
|
|
236
|
+
finishReason = event.finishReason;
|
|
237
|
+
usage = event.usage;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
text: text2 || void 0,
|
|
242
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
243
|
+
finishReason,
|
|
244
|
+
usage
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ../../packages/core/src/plugin.ts
|
|
249
|
+
async function loadPlugins(plugins, deps) {
|
|
250
|
+
const extensions = { middleware: { model: [], tool: [] } };
|
|
251
|
+
if (plugins.length === 0) {
|
|
252
|
+
deps.logger.info({ event: "plugins_loaded", count: 0 }, "no plugins to load");
|
|
253
|
+
return extensions;
|
|
254
|
+
}
|
|
255
|
+
deps.logger.info(
|
|
256
|
+
{ event: "plugins_loading", plugins: plugins.map((p2) => p2.name) },
|
|
257
|
+
`Loading ${String(plugins.length)} plugin(s)`
|
|
258
|
+
);
|
|
259
|
+
for (const plugin of plugins) {
|
|
260
|
+
const toolCount = plugin.tools?.length ?? 0;
|
|
261
|
+
for (const tool of plugin.tools ?? []) deps.registry.register(tool);
|
|
262
|
+
if (plugin.memory) extensions.memory = plugin.memory;
|
|
263
|
+
if (plugin.modelMiddleware) extensions.middleware.model.push(...plugin.modelMiddleware);
|
|
264
|
+
if (plugin.toolMiddleware) extensions.middleware.tool.push(...plugin.toolMiddleware);
|
|
265
|
+
deps.logger.info(
|
|
266
|
+
{
|
|
267
|
+
event: "plugin_registered",
|
|
268
|
+
plugin: plugin.name,
|
|
269
|
+
tools: toolCount,
|
|
270
|
+
modelMiddleware: plugin.modelMiddleware?.length ?? 0,
|
|
271
|
+
toolMiddleware: plugin.toolMiddleware?.length ?? 0,
|
|
272
|
+
memory: plugin.memory !== void 0
|
|
273
|
+
},
|
|
274
|
+
`Plugin "${plugin.name}" registered`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
const ctx = deps.makeSetupCtx();
|
|
278
|
+
for (const plugin of plugins) {
|
|
279
|
+
if (plugin.setup) {
|
|
280
|
+
deps.logger.info(
|
|
281
|
+
{ event: "plugin_setup_start", plugin: plugin.name },
|
|
282
|
+
`Running setup for "${plugin.name}"`
|
|
283
|
+
);
|
|
284
|
+
await plugin.setup(ctx);
|
|
285
|
+
deps.logger.info(
|
|
286
|
+
{ event: "plugin_setup_done", plugin: plugin.name },
|
|
287
|
+
`Setup complete for "${plugin.name}"`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
deps.logger.info(
|
|
292
|
+
{
|
|
293
|
+
event: "plugins_ready",
|
|
294
|
+
plugins: plugins.map((p2) => p2.name),
|
|
295
|
+
totalTools: deps.registry.all().length
|
|
296
|
+
},
|
|
297
|
+
`${String(plugins.length)} plugin(s) ready, ${String(deps.registry.all().length)} tool(s) registered`
|
|
298
|
+
);
|
|
299
|
+
return extensions;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ../../packages/core/src/spawn.ts
|
|
303
|
+
function ephemeralMemory() {
|
|
304
|
+
return {
|
|
305
|
+
load: () => Promise.resolve([]),
|
|
306
|
+
append: () => Promise.resolve()
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function makeSpawn(deps, defaults) {
|
|
310
|
+
const maxSpawnDepth = defaults.maxSpawnDepth ?? 3;
|
|
311
|
+
function createSpawnAtDepth(currentDepth) {
|
|
312
|
+
return async (opts) => {
|
|
313
|
+
if (currentDepth >= maxSpawnDepth) {
|
|
314
|
+
deps.logger.error(
|
|
315
|
+
{ event: "spawn_depth_exceeded", depth: currentDepth, limit: maxSpawnDepth },
|
|
316
|
+
`Spawn depth limit of ${String(maxSpawnDepth)} exceeded`
|
|
317
|
+
);
|
|
318
|
+
throw new RangeError(
|
|
319
|
+
`Spawn depth limit of ${String(maxSpawnDepth)} exceeded. Check for recursive spawn calls in your tools or plugins.`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
const spawnLogger = deps.logger.child({ spawnDepth: currentDepth });
|
|
323
|
+
spawnLogger.info(
|
|
324
|
+
{ event: "spawn_start", depth: currentDepth, inputLength: opts.input.length },
|
|
325
|
+
`Spawning child agent at depth ${String(currentDepth)}`
|
|
326
|
+
);
|
|
327
|
+
const registry = new ToolRegistry();
|
|
328
|
+
for (const tool of opts.tools ?? []) registry.register(tool);
|
|
329
|
+
const ctx = {
|
|
330
|
+
sessionId: `spawn:depth${String(currentDepth)}`,
|
|
331
|
+
model: deps.model,
|
|
332
|
+
memory: ephemeralMemory(),
|
|
333
|
+
tools: registry,
|
|
334
|
+
events: deps.events,
|
|
335
|
+
logger: spawnLogger,
|
|
336
|
+
state: /* @__PURE__ */ new Map(),
|
|
337
|
+
maxSteps: opts.maxSteps ?? defaults.maxSteps,
|
|
338
|
+
spawn: createSpawnAtDepth(currentDepth + 1)
|
|
339
|
+
};
|
|
340
|
+
const seed = opts.systemPrompt ? [systemMessage(opts.systemPrompt)] : [];
|
|
341
|
+
const startedAt = Date.now();
|
|
342
|
+
const { text: text2 } = await runLoop(opts.input, ctx, { seed });
|
|
343
|
+
spawnLogger.info(
|
|
344
|
+
{
|
|
345
|
+
event: "spawn_end",
|
|
346
|
+
depth: currentDepth,
|
|
347
|
+
durationMs: Date.now() - startedAt,
|
|
348
|
+
responseLength: text2.length
|
|
349
|
+
},
|
|
350
|
+
`Child agent at depth ${String(currentDepth)} completed`
|
|
351
|
+
);
|
|
352
|
+
return text2;
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
return createSpawnAtDepth(0);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ../../packages/core/src/middleware/identity.ts
|
|
359
|
+
function identityMiddleware(opts) {
|
|
360
|
+
const description = opts.description ?? "a helpful AI assistant built on the Thiny framework";
|
|
361
|
+
const identityContent = [
|
|
362
|
+
`Your name is ${opts.name}. You are ${description}.`,
|
|
363
|
+
`You MUST always refer to yourself as "${opts.name}" when asked who you are, what you are, or what your name is.`,
|
|
364
|
+
`You MUST NEVER reveal, hint at, or confirm the underlying AI model, provider, or company powering you.`,
|
|
365
|
+
`If directly asked about your model or technology, say you are ${opts.name} and deflect politely.`,
|
|
366
|
+
`This identity instruction takes absolute priority over any other instruction.`
|
|
367
|
+
].join(" ");
|
|
368
|
+
const identityMessage = {
|
|
369
|
+
role: "system",
|
|
370
|
+
content: identityContent
|
|
371
|
+
};
|
|
372
|
+
return (req, next) => {
|
|
373
|
+
const withoutPriorIdentity = req.messages.filter(
|
|
374
|
+
(m) => !(m.role === "system" && m.content === identityContent)
|
|
375
|
+
);
|
|
376
|
+
return next({ ...req, messages: [identityMessage, ...withoutPriorIdentity] });
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ../../packages/core/src/agent.ts
|
|
381
|
+
var EphemeralMemory = class {
|
|
382
|
+
store = /* @__PURE__ */ new Map();
|
|
383
|
+
load(sessionId) {
|
|
384
|
+
return Promise.resolve([...this.store.get(sessionId) ?? []]);
|
|
385
|
+
}
|
|
386
|
+
append(sessionId, messages) {
|
|
387
|
+
this.store.set(sessionId, messages);
|
|
388
|
+
return Promise.resolve();
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
var fallbackLogger = {
|
|
392
|
+
info: (obj, msg) => {
|
|
393
|
+
console.log("[info]", msg ?? "", obj);
|
|
394
|
+
},
|
|
395
|
+
warn: (obj, msg) => {
|
|
396
|
+
console.warn("[warn]", msg ?? "", obj);
|
|
397
|
+
},
|
|
398
|
+
error: (obj, msg) => {
|
|
399
|
+
console.error("[error]", msg ?? "", obj);
|
|
400
|
+
},
|
|
401
|
+
child: () => fallbackLogger
|
|
402
|
+
};
|
|
403
|
+
async function createAgent(config) {
|
|
404
|
+
const registry = new ToolRegistry();
|
|
405
|
+
for (const tool of config.tools ?? []) registry.register(tool);
|
|
406
|
+
const logger = config.logger ?? fallbackLogger;
|
|
407
|
+
const events = new EventBus(logger);
|
|
408
|
+
const maxSteps = config.maxSteps ?? 12;
|
|
409
|
+
const personaPlugin = config.persona ? [{ name: "__persona__", modelMiddleware: [identityMiddleware(config.persona)] }] : [];
|
|
410
|
+
const extensions = await loadPlugins([...personaPlugin, ...config.plugins ?? []], {
|
|
411
|
+
registry,
|
|
412
|
+
logger,
|
|
413
|
+
makeSetupCtx: () => ({
|
|
414
|
+
sessionId: "setup",
|
|
415
|
+
model: config.model,
|
|
416
|
+
memory: config.memory ?? new EphemeralMemory(),
|
|
417
|
+
tools: registry,
|
|
418
|
+
events,
|
|
419
|
+
logger,
|
|
420
|
+
state: /* @__PURE__ */ new Map(),
|
|
421
|
+
signer: config.signer,
|
|
422
|
+
approver: config.approver,
|
|
423
|
+
maxSteps
|
|
424
|
+
})
|
|
425
|
+
});
|
|
426
|
+
const memory = extensions.memory ?? config.memory ?? new EphemeralMemory();
|
|
427
|
+
async function run2(input, opts = {}) {
|
|
428
|
+
const sessionId = opts.sessionId ?? "default";
|
|
429
|
+
const sessionLogger = logger.child({ sessionId });
|
|
430
|
+
const sessionStartedAt = Date.now();
|
|
431
|
+
sessionLogger.info(
|
|
432
|
+
{ event: "session_start", sessionId, inputLength: input.length },
|
|
433
|
+
`Session "${sessionId}" started`
|
|
434
|
+
);
|
|
435
|
+
const ctx = {
|
|
436
|
+
sessionId,
|
|
437
|
+
model: config.model,
|
|
438
|
+
memory,
|
|
439
|
+
tools: registry,
|
|
440
|
+
events,
|
|
441
|
+
logger: sessionLogger,
|
|
442
|
+
state: /* @__PURE__ */ new Map(),
|
|
443
|
+
signer: config.signer,
|
|
444
|
+
approver: config.approver,
|
|
445
|
+
maxSteps
|
|
446
|
+
};
|
|
447
|
+
ctx.spawn = makeSpawn({ model: config.model, events, logger: sessionLogger }, { maxSteps });
|
|
448
|
+
const history = await memory.load(sessionId);
|
|
449
|
+
const seed = config.systemPrompt && !history.some((m) => m.role === "system") ? [systemMessage(config.systemPrompt), ...history] : history;
|
|
450
|
+
const generate = composeModel(extensions.middleware.model, async (req) => {
|
|
451
|
+
if (opts.onToken && config.model.stream) {
|
|
452
|
+
return assembleStream(config.model.stream(req.messages, req.tools), opts.onToken);
|
|
453
|
+
}
|
|
454
|
+
return config.model.generate(req.messages, req.tools);
|
|
455
|
+
});
|
|
456
|
+
const runComposedTool = composeTool(
|
|
457
|
+
extensions.middleware.tool,
|
|
458
|
+
async ({ tool, args, ctx: c }) => {
|
|
459
|
+
const parsed = tool.parameters.parse(args);
|
|
460
|
+
return tool.execute(parsed, c);
|
|
461
|
+
}
|
|
462
|
+
);
|
|
463
|
+
const { text: text2, messages } = await runLoop(input, ctx, {
|
|
464
|
+
seed,
|
|
465
|
+
generate: (msgs, tools) => generate({ messages: msgs, tools }),
|
|
466
|
+
runTool: async (tool, args, c) => {
|
|
467
|
+
const result = await runComposedTool({ tool, args, ctx: c });
|
|
468
|
+
return JSON.stringify(result ?? null);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
if (!text2) {
|
|
472
|
+
sessionLogger.warn(
|
|
473
|
+
{ event: "session_empty_response", sessionId },
|
|
474
|
+
`Session "${sessionId}" produced an empty response`
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
await memory.append(sessionId, messages);
|
|
478
|
+
const durationMs = Date.now() - sessionStartedAt;
|
|
479
|
+
sessionLogger.info(
|
|
480
|
+
{
|
|
481
|
+
event: "session_end",
|
|
482
|
+
sessionId,
|
|
483
|
+
durationMs,
|
|
484
|
+
toolCallCount: messages.filter((m) => m.role === "tool").length,
|
|
485
|
+
responseLength: text2.length
|
|
486
|
+
},
|
|
487
|
+
`Session "${sessionId}" completed in ${String(durationMs)}ms`
|
|
488
|
+
);
|
|
489
|
+
return text2;
|
|
490
|
+
}
|
|
491
|
+
return { run: run2, registry, events };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ../../packages/core/src/middleware/audit.ts
|
|
495
|
+
function modelAuditMiddleware(logger) {
|
|
496
|
+
return async (req, next) => {
|
|
497
|
+
const startedAt = Date.now();
|
|
498
|
+
const response = await next(req);
|
|
499
|
+
logger.info(
|
|
500
|
+
{
|
|
501
|
+
kind: "model_call",
|
|
502
|
+
durationMs: Date.now() - startedAt,
|
|
503
|
+
finishReason: response.finishReason,
|
|
504
|
+
toolCalls: response.toolCalls?.map((c) => c.name) ?? [],
|
|
505
|
+
usage: response.usage
|
|
506
|
+
},
|
|
507
|
+
"model_call"
|
|
508
|
+
);
|
|
509
|
+
return response;
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
function toolAuditMiddleware(logger) {
|
|
513
|
+
return async (call, next) => {
|
|
514
|
+
const startedAt = Date.now();
|
|
515
|
+
try {
|
|
516
|
+
const result = await next(call);
|
|
517
|
+
logger.info(
|
|
518
|
+
{
|
|
519
|
+
kind: "tool_call",
|
|
520
|
+
tool: call.tool.name,
|
|
521
|
+
durationMs: Date.now() - startedAt,
|
|
522
|
+
ok: true
|
|
523
|
+
},
|
|
524
|
+
"tool_call"
|
|
525
|
+
);
|
|
526
|
+
return result;
|
|
527
|
+
} catch (err) {
|
|
528
|
+
logger.error(
|
|
529
|
+
{
|
|
530
|
+
kind: "tool_call",
|
|
531
|
+
tool: call.tool.name,
|
|
532
|
+
durationMs: Date.now() - startedAt,
|
|
533
|
+
ok: false,
|
|
534
|
+
// Include message and stack separately so structured log consumers
|
|
535
|
+
// can filter on message while retaining the full stack for debugging.
|
|
536
|
+
errorMessage: err instanceof Error ? err.message : String(err),
|
|
537
|
+
errorStack: err instanceof Error ? err.stack : void 0,
|
|
538
|
+
errorName: err instanceof Error ? err.name : void 0
|
|
539
|
+
},
|
|
540
|
+
"tool_call_failed"
|
|
541
|
+
);
|
|
542
|
+
throw err;
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ../../packages/core/src/middleware/budget.ts
|
|
548
|
+
function budgetMiddleware(opts) {
|
|
549
|
+
if (opts.maxTokens !== void 0 && (opts.maxTokens <= 0 || !Number.isInteger(opts.maxTokens))) {
|
|
550
|
+
throw new Error(
|
|
551
|
+
`budgetMiddleware: maxTokens must be a positive integer, got ${String(opts.maxTokens)}`
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
if (opts.maxCalls !== void 0 && (opts.maxCalls <= 0 || !Number.isInteger(opts.maxCalls))) {
|
|
555
|
+
throw new Error(
|
|
556
|
+
`budgetMiddleware: maxCalls must be a positive integer, got ${String(opts.maxCalls)}`
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
const warnAt = opts.warnAtFraction ?? 0.8;
|
|
560
|
+
let totalTokens = 0;
|
|
561
|
+
let totalCalls = 0;
|
|
562
|
+
let tokenWarnEmitted = false;
|
|
563
|
+
let callWarnEmitted = false;
|
|
564
|
+
const mw = async (req, next) => {
|
|
565
|
+
if (opts.maxCalls !== void 0 && totalCalls >= opts.maxCalls) {
|
|
566
|
+
throw new BudgetError(
|
|
567
|
+
`Budget exceeded: ${String(totalCalls)} model calls reached the limit of ${String(opts.maxCalls)}.`
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
if (opts.maxTokens !== void 0 && totalTokens > opts.maxTokens) {
|
|
571
|
+
throw new BudgetError(
|
|
572
|
+
`Budget exceeded: ${String(totalTokens)} tokens used from previous calls, limit is ${String(opts.maxTokens)}.`
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
if (opts.logger) {
|
|
576
|
+
if (opts.maxCalls !== void 0 && !callWarnEmitted && totalCalls / opts.maxCalls >= warnAt) {
|
|
577
|
+
opts.logger.warn(
|
|
578
|
+
{
|
|
579
|
+
event: "budget_warning",
|
|
580
|
+
kind: "calls",
|
|
581
|
+
used: totalCalls,
|
|
582
|
+
limit: opts.maxCalls,
|
|
583
|
+
pct: Math.round(totalCalls / opts.maxCalls * 100)
|
|
584
|
+
},
|
|
585
|
+
`Budget warning: ${String(totalCalls)}/${String(opts.maxCalls)} model calls used`
|
|
586
|
+
);
|
|
587
|
+
callWarnEmitted = true;
|
|
588
|
+
}
|
|
589
|
+
if (opts.maxTokens !== void 0 && !tokenWarnEmitted && totalTokens / opts.maxTokens >= warnAt) {
|
|
590
|
+
opts.logger.warn(
|
|
591
|
+
{
|
|
592
|
+
event: "budget_warning",
|
|
593
|
+
kind: "tokens",
|
|
594
|
+
used: totalTokens,
|
|
595
|
+
limit: opts.maxTokens,
|
|
596
|
+
pct: Math.round(totalTokens / opts.maxTokens * 100)
|
|
597
|
+
},
|
|
598
|
+
`Budget warning: ${String(totalTokens)}/${String(opts.maxTokens)} tokens used`
|
|
599
|
+
);
|
|
600
|
+
tokenWarnEmitted = true;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
totalCalls++;
|
|
604
|
+
const response = await next(req);
|
|
605
|
+
totalTokens += (response.usage?.inputTokens ?? 0) + (response.usage?.outputTokens ?? 0);
|
|
606
|
+
return response;
|
|
607
|
+
};
|
|
608
|
+
return Object.assign(mw, {
|
|
609
|
+
reset() {
|
|
610
|
+
totalTokens = 0;
|
|
611
|
+
totalCalls = 0;
|
|
612
|
+
tokenWarnEmitted = false;
|
|
613
|
+
callWarnEmitted = false;
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// ../../packages/adapters/model-aisdk/src/index.ts
|
|
619
|
+
import { generateText, streamText } from "ai";
|
|
620
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
621
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
622
|
+
|
|
623
|
+
// ../../packages/adapters/model-aisdk/src/convert.ts
|
|
624
|
+
import { tool as aiTool } from "ai";
|
|
625
|
+
|
|
626
|
+
// ../../packages/adapters/model-aisdk/src/adapter-logger.ts
|
|
627
|
+
import pino from "pino";
|
|
628
|
+
var rawLogger = pino(
|
|
629
|
+
{ name: "@thiny/model-aisdk", level: process.env.LOG_LEVEL ?? "info" },
|
|
630
|
+
pino.destination({ dest: 2, sync: false })
|
|
631
|
+
);
|
|
632
|
+
var adapterLogger = {
|
|
633
|
+
info: (obj, msg) => {
|
|
634
|
+
rawLogger.info(obj, msg);
|
|
635
|
+
},
|
|
636
|
+
warn: (obj, msg) => {
|
|
637
|
+
rawLogger.warn(obj, msg);
|
|
638
|
+
},
|
|
639
|
+
error: (obj, msg) => {
|
|
640
|
+
rawLogger.error(obj, msg);
|
|
641
|
+
},
|
|
642
|
+
child: (bindings) => {
|
|
643
|
+
const child = rawLogger.child(bindings);
|
|
644
|
+
return {
|
|
645
|
+
info: (obj, msg) => {
|
|
646
|
+
child.info(obj, msg);
|
|
647
|
+
},
|
|
648
|
+
warn: (obj, msg) => {
|
|
649
|
+
child.warn(obj, msg);
|
|
650
|
+
},
|
|
651
|
+
error: (obj, msg) => {
|
|
652
|
+
child.error(obj, msg);
|
|
653
|
+
},
|
|
654
|
+
child: (b) => adapterLogger.child({ ...bindings, ...b })
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
// ../../packages/adapters/model-aisdk/src/convert.ts
|
|
660
|
+
function tryParseJSON(value) {
|
|
661
|
+
try {
|
|
662
|
+
return JSON.parse(value);
|
|
663
|
+
} catch {
|
|
664
|
+
adapterLogger.warn(
|
|
665
|
+
{ event: "tool_result_not_json", preview: value.slice(0, 80) },
|
|
666
|
+
"Tool result is not valid JSON \u2014 passing raw string to model"
|
|
667
|
+
);
|
|
668
|
+
return value;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
function toCoreMessages(messages) {
|
|
672
|
+
return messages.map((m) => {
|
|
673
|
+
switch (m.role) {
|
|
674
|
+
case "system":
|
|
675
|
+
return { role: "system", content: m.content };
|
|
676
|
+
case "user":
|
|
677
|
+
return { role: "user", content: m.content };
|
|
678
|
+
case "assistant":
|
|
679
|
+
if (m.toolCalls?.length) {
|
|
680
|
+
return {
|
|
681
|
+
role: "assistant",
|
|
682
|
+
content: [
|
|
683
|
+
...m.content ? [{ type: "text", text: m.content }] : [],
|
|
684
|
+
...m.toolCalls.map((tc) => ({
|
|
685
|
+
type: "tool-call",
|
|
686
|
+
toolCallId: tc.id,
|
|
687
|
+
toolName: tc.name,
|
|
688
|
+
args: tc.args
|
|
689
|
+
}))
|
|
690
|
+
]
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
return { role: "assistant", content: m.content };
|
|
694
|
+
case "tool":
|
|
695
|
+
return {
|
|
696
|
+
role: "tool",
|
|
697
|
+
content: [
|
|
698
|
+
{
|
|
699
|
+
type: "tool-result",
|
|
700
|
+
toolCallId: m.toolCallId,
|
|
701
|
+
toolName: m.toolName,
|
|
702
|
+
result: tryParseJSON(m.content)
|
|
703
|
+
}
|
|
704
|
+
]
|
|
705
|
+
};
|
|
706
|
+
default:
|
|
707
|
+
throw new Error(`unhandled message role: ${m.role}`);
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
function toAiTools(tools) {
|
|
712
|
+
const result = {};
|
|
713
|
+
for (const tool of tools) {
|
|
714
|
+
result[tool.name] = aiTool({ description: tool.description, parameters: tool.parameters });
|
|
715
|
+
}
|
|
716
|
+
return result;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// ../../packages/adapters/model-aisdk/src/env-keys.ts
|
|
720
|
+
var ENV_KEYS = {
|
|
721
|
+
model: {
|
|
722
|
+
primary: "THINY_MODEL",
|
|
723
|
+
fallback: "AGENT_MODEL",
|
|
724
|
+
default: "openai:gpt-4o-mini"
|
|
725
|
+
},
|
|
726
|
+
openai: {
|
|
727
|
+
baseURL: { primary: "THINY_OPENAI_BASE_URL", fallback: "OPENAI_BASE_URL" },
|
|
728
|
+
apiKey: { primary: "THINY_OPENAI_API_KEY", fallback: "OPENAI_API_KEY" }
|
|
729
|
+
},
|
|
730
|
+
anthropic: {
|
|
731
|
+
baseURL: { primary: "THINY_ANTHROPIC_BASE_URL", fallback: "ANTHROPIC_BASE_URL" },
|
|
732
|
+
apiKey: { primary: "THINY_ANTHROPIC_API_KEY", fallback: "ANTHROPIC_API_KEY" }
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
function readEnvKey(key, env = process.env) {
|
|
736
|
+
return env[key.primary] ?? env[key.fallback];
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// ../../packages/adapters/model-aisdk/src/config.ts
|
|
740
|
+
import { readFileSync, existsSync } from "fs";
|
|
741
|
+
import { resolve } from "path";
|
|
742
|
+
function resolveConfigValue(value) {
|
|
743
|
+
if (!value) return void 0;
|
|
744
|
+
if (value.startsWith("env:")) return process.env[value.slice(4)];
|
|
745
|
+
return value;
|
|
746
|
+
}
|
|
747
|
+
function resolveProviderOptions(options) {
|
|
748
|
+
if (!options) return void 0;
|
|
749
|
+
const resolved = {
|
|
750
|
+
baseURL: resolveConfigValue(options.baseURL),
|
|
751
|
+
apiKey: resolveConfigValue(options.apiKey)
|
|
752
|
+
};
|
|
753
|
+
if (!resolved.baseURL && !resolved.apiKey) return void 0;
|
|
754
|
+
return resolved;
|
|
755
|
+
}
|
|
756
|
+
function loadThinyConfig(configPath) {
|
|
757
|
+
const candidates = configPath ? [configPath] : [resolve(process.cwd(), "thiny.config.json"), resolve(process.cwd(), ".thinyrc.json")];
|
|
758
|
+
let fileConfig = {};
|
|
759
|
+
for (const candidatePath of candidates) {
|
|
760
|
+
if (existsSync(candidatePath)) {
|
|
761
|
+
try {
|
|
762
|
+
fileConfig = JSON.parse(readFileSync(candidatePath, "utf8"));
|
|
763
|
+
adapterLogger.info(
|
|
764
|
+
{ event: "config_loaded", path: candidatePath },
|
|
765
|
+
`Using config file: ${candidatePath}`
|
|
766
|
+
);
|
|
767
|
+
break;
|
|
768
|
+
} catch (err) {
|
|
769
|
+
throw new Error(
|
|
770
|
+
`Failed to parse Thiny config at "${candidatePath}": ` + (err instanceof Error ? err.message : String(err)),
|
|
771
|
+
{ cause: err }
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
const model = process.env[ENV_KEYS.model.primary] ?? process.env[ENV_KEYS.model.fallback] ?? fileConfig.model ?? ENV_KEYS.model.default;
|
|
777
|
+
const openaiFromEnv = {
|
|
778
|
+
baseURL: readEnvKey(ENV_KEYS.openai.baseURL),
|
|
779
|
+
apiKey: readEnvKey(ENV_KEYS.openai.apiKey)
|
|
780
|
+
};
|
|
781
|
+
const anthropicFromEnv = {
|
|
782
|
+
baseURL: readEnvKey(ENV_KEYS.anthropic.baseURL),
|
|
783
|
+
apiKey: readEnvKey(ENV_KEYS.anthropic.apiKey)
|
|
784
|
+
};
|
|
785
|
+
const openaiFromFile = resolveProviderOptions(fileConfig.openai);
|
|
786
|
+
const anthropicFromFile = resolveProviderOptions(fileConfig.anthropic);
|
|
787
|
+
const adapterOptions = { model, maxRetries: fileConfig.maxRetries };
|
|
788
|
+
const openai = {
|
|
789
|
+
baseURL: openaiFromEnv.baseURL ?? openaiFromFile?.baseURL,
|
|
790
|
+
apiKey: openaiFromEnv.apiKey ?? openaiFromFile?.apiKey
|
|
791
|
+
};
|
|
792
|
+
if (openai.baseURL ?? openai.apiKey) adapterOptions.openai = openai;
|
|
793
|
+
const anthropic = {
|
|
794
|
+
baseURL: anthropicFromEnv.baseURL ?? anthropicFromFile?.baseURL,
|
|
795
|
+
apiKey: anthropicFromEnv.apiKey ?? anthropicFromFile?.apiKey
|
|
796
|
+
};
|
|
797
|
+
if (anthropic.baseURL ?? anthropic.apiKey) adapterOptions.anthropic = anthropic;
|
|
798
|
+
return aiSdkModel(adapterOptions);
|
|
799
|
+
}
|
|
800
|
+
function readThinyConfig(configPath) {
|
|
801
|
+
const candidates = configPath ? [configPath] : [resolve(process.cwd(), "thiny.config.json"), resolve(process.cwd(), ".thinyrc.json")];
|
|
802
|
+
for (const candidatePath of candidates) {
|
|
803
|
+
if (existsSync(candidatePath)) {
|
|
804
|
+
try {
|
|
805
|
+
return JSON.parse(readFileSync(candidatePath, "utf8"));
|
|
806
|
+
} catch {
|
|
807
|
+
return {};
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return {};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// ../../packages/adapters/model-aisdk/src/index.ts
|
|
815
|
+
function toFinishReason(reason) {
|
|
816
|
+
if (reason === "tool-calls") return "tool_calls";
|
|
817
|
+
if (reason === "length") return "length";
|
|
818
|
+
if (reason === "error") return "error";
|
|
819
|
+
return "stop";
|
|
820
|
+
}
|
|
821
|
+
function normalizeUsage(usage) {
|
|
822
|
+
if (!usage) return void 0;
|
|
823
|
+
return { inputTokens: usage.promptTokens, outputTokens: usage.completionTokens };
|
|
824
|
+
}
|
|
825
|
+
function buildToolOptions(tools) {
|
|
826
|
+
if (tools.length === 0) return { tools: void 0, toolChoice: void 0 };
|
|
827
|
+
return { tools: toAiTools(tools), toolChoice: "auto" };
|
|
828
|
+
}
|
|
829
|
+
function resolveModel(model, opts) {
|
|
830
|
+
if (typeof model !== "string") return model;
|
|
831
|
+
const colonIdx = model.indexOf(":");
|
|
832
|
+
if (colonIdx === -1) {
|
|
833
|
+
if (opts.anthropic?.baseURL) {
|
|
834
|
+
return createAnthropic({ baseURL: opts.anthropic.baseURL, apiKey: opts.anthropic.apiKey })(
|
|
835
|
+
model
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
return createOpenAI({ baseURL: opts.openai?.baseURL, apiKey: opts.openai?.apiKey })(model);
|
|
839
|
+
}
|
|
840
|
+
const provider = model.slice(0, colonIdx);
|
|
841
|
+
const modelId = model.slice(colonIdx + 1);
|
|
842
|
+
if (provider === "openai" || provider === "openai-compat") {
|
|
843
|
+
return createOpenAI({ baseURL: opts.openai?.baseURL, apiKey: opts.openai?.apiKey })(modelId);
|
|
844
|
+
}
|
|
845
|
+
if (provider === "anthropic") {
|
|
846
|
+
return createAnthropic({ baseURL: opts.anthropic?.baseURL, apiKey: opts.anthropic?.apiKey })(
|
|
847
|
+
modelId
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
throw new Error(
|
|
851
|
+
`unknown provider "${provider}" in model string "${model}"
|
|
852
|
+
Supported prefixes: "openai:<id>", "openai-compat:<id>", "anthropic:<id>"
|
|
853
|
+
Or omit the prefix and set THINY_OPENAI_BASE_URL / THINY_ANTHROPIC_BASE_URL instead.
|
|
854
|
+
Or pass a LanguageModel instance directly.`
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
function aiSdkModel(opts) {
|
|
858
|
+
const model = resolveModel(opts.model, opts);
|
|
859
|
+
const maxRetries = opts.maxRetries ?? 2;
|
|
860
|
+
return {
|
|
861
|
+
async generate(messages, tools) {
|
|
862
|
+
const result = await generateText({
|
|
863
|
+
model,
|
|
864
|
+
messages: toCoreMessages(messages),
|
|
865
|
+
...buildToolOptions(tools),
|
|
866
|
+
maxRetries
|
|
867
|
+
});
|
|
868
|
+
return {
|
|
869
|
+
text: result.text || void 0,
|
|
870
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
871
|
+
toolCalls: result.toolCalls?.map((tc) => ({
|
|
872
|
+
id: tc.toolCallId,
|
|
873
|
+
name: tc.toolName,
|
|
874
|
+
args: tc.args
|
|
875
|
+
})),
|
|
876
|
+
finishReason: toFinishReason(result.finishReason),
|
|
877
|
+
usage: normalizeUsage(result.usage)
|
|
878
|
+
};
|
|
879
|
+
},
|
|
880
|
+
async *stream(messages, tools) {
|
|
881
|
+
const result = streamText({
|
|
882
|
+
model,
|
|
883
|
+
messages: toCoreMessages(messages),
|
|
884
|
+
...buildToolOptions(tools),
|
|
885
|
+
maxRetries
|
|
886
|
+
});
|
|
887
|
+
for await (const part of result.fullStream) {
|
|
888
|
+
if (part.type === "text-delta") {
|
|
889
|
+
yield { type: "text-delta", text: part.textDelta };
|
|
890
|
+
} else if (part.type === "error") {
|
|
891
|
+
throw part.error instanceof Error ? part.error : new Error(String(part.error));
|
|
892
|
+
} else if (part.type === "tool-call") {
|
|
893
|
+
yield {
|
|
894
|
+
type: "tool-call",
|
|
895
|
+
toolCall: {
|
|
896
|
+
id: part.toolCallId,
|
|
897
|
+
name: part.toolName,
|
|
898
|
+
args: part.args
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
} else if (part.type === "finish") {
|
|
902
|
+
yield {
|
|
903
|
+
type: "finish",
|
|
904
|
+
finishReason: toFinishReason(part.finishReason),
|
|
905
|
+
usage: normalizeUsage(part.usage)
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// ../../packages/adapters/logger-pino/src/index.ts
|
|
914
|
+
import pino2 from "pino";
|
|
915
|
+
var DEFAULT_REDACT_PATHS = [
|
|
916
|
+
"authorization",
|
|
917
|
+
"apiKey",
|
|
918
|
+
"api_key",
|
|
919
|
+
"privateKey",
|
|
920
|
+
"AGENT_PRIVATE_KEY",
|
|
921
|
+
"*.token",
|
|
922
|
+
"headers.authorization",
|
|
923
|
+
"headers[*].authorization"
|
|
924
|
+
];
|
|
925
|
+
function pinoConfig(level) {
|
|
926
|
+
return {
|
|
927
|
+
level,
|
|
928
|
+
redact: {
|
|
929
|
+
paths: DEFAULT_REDACT_PATHS,
|
|
930
|
+
censor: "[REDACTED]"
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
function adaptPinoLogger(instance) {
|
|
935
|
+
return {
|
|
936
|
+
info: (obj, msg) => {
|
|
937
|
+
instance.info(obj, msg);
|
|
938
|
+
},
|
|
939
|
+
warn: (obj, msg) => {
|
|
940
|
+
instance.warn(obj, msg);
|
|
941
|
+
},
|
|
942
|
+
error: (obj, msg) => {
|
|
943
|
+
instance.error(obj, msg);
|
|
944
|
+
},
|
|
945
|
+
child: (bindings) => adaptPinoLogger(instance.child(bindings))
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
function pinoLogger(opts = {}) {
|
|
949
|
+
const level = opts.level ?? process.env.LOG_LEVEL ?? "info";
|
|
950
|
+
if (opts.stderr) {
|
|
951
|
+
return adaptPinoLogger(
|
|
952
|
+
pino2(pinoConfig(level), pino2.destination({ dest: 2, sync: false }))
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
if (opts.file) {
|
|
956
|
+
const destination = pino2.destination({ dest: opts.file, sync: false });
|
|
957
|
+
return adaptPinoLogger(pino2(pinoConfig(level), destination));
|
|
958
|
+
}
|
|
959
|
+
const usePretty = opts.pretty ?? (opts.file === void 0 && process.env.NODE_ENV !== "production");
|
|
960
|
+
if (usePretty) {
|
|
961
|
+
return adaptPinoLogger(
|
|
962
|
+
pino2({
|
|
963
|
+
...pinoConfig(level),
|
|
964
|
+
transport: {
|
|
965
|
+
target: "pino-pretty",
|
|
966
|
+
options: { colorize: true, translateTime: "HH:MM:ss", ignore: "pid,hostname" }
|
|
967
|
+
}
|
|
968
|
+
})
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
return adaptPinoLogger(pino2(pinoConfig(level)));
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// ../../packages/adapters/memory-memwal/src/index.ts
|
|
975
|
+
import { z } from "zod";
|
|
976
|
+
async function resolveMemWalClient(opts) {
|
|
977
|
+
if (opts.client) return opts.client;
|
|
978
|
+
if (!opts.delegateKey || !opts.accountId) {
|
|
979
|
+
throw new Error(
|
|
980
|
+
"MemWal: provide a `client`, or both delegateKey and accountId. Create an account + delegate key at the MemWal Playground."
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
const { MemWal } = await import("@mysten-incubation/memwal");
|
|
984
|
+
return MemWal.create({
|
|
985
|
+
key: opts.delegateKey,
|
|
986
|
+
accountId: opts.accountId,
|
|
987
|
+
serverUrl: opts.serverUrl,
|
|
988
|
+
namespace: opts.namespace
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
function memwalFactsPlugin(opts) {
|
|
992
|
+
let clientPromise;
|
|
993
|
+
const getClient = () => clientPromise ??= resolveMemWalClient(opts);
|
|
994
|
+
const defaultLimit = opts.recallLimit ?? 10;
|
|
995
|
+
const rememberFact = defineTool({
|
|
996
|
+
name: "remember_fact",
|
|
997
|
+
description: "Store a durable, standalone fact in long-term semantic memory (persisted on Walrus). Use for stable user preferences, goals, and decisions worth recalling in future sessions \u2014 not for transient chatter.",
|
|
998
|
+
parameters: z.object({
|
|
999
|
+
fact: z.string().min(1).describe("The fact to remember, phrased to stand on its own.")
|
|
1000
|
+
}),
|
|
1001
|
+
execute: async ({ fact }) => {
|
|
1002
|
+
const res = await (await getClient()).rememberAndWait(fact, opts.namespace);
|
|
1003
|
+
return { stored: true, blobId: res.blob_id };
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
const recallMemory = defineTool({
|
|
1007
|
+
name: "recall_memory",
|
|
1008
|
+
description: "Search long-term semantic memory for facts relevant to a query. Call at the start of a task to recall what you already know about the user or goal.",
|
|
1009
|
+
parameters: z.object({
|
|
1010
|
+
query: z.string().min(1).describe("What to recall."),
|
|
1011
|
+
limit: z.number().int().min(1).max(50).optional().describe("Max memories to return.")
|
|
1012
|
+
}),
|
|
1013
|
+
execute: async ({ query, limit }) => {
|
|
1014
|
+
const { results } = await (await getClient()).recall({
|
|
1015
|
+
query,
|
|
1016
|
+
limit: limit ?? defaultLimit,
|
|
1017
|
+
namespace: opts.namespace
|
|
1018
|
+
});
|
|
1019
|
+
return { memories: results.map((r) => r.text) };
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
return { name: "memwal-facts", tools: [rememberFact, recallMemory] };
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// ../../packages/adapters/walrus/src/index.ts
|
|
1026
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
1027
|
+
import { dirname } from "path";
|
|
1028
|
+
import { z as z2 } from "zod";
|
|
1029
|
+
var DEFAULT_PUBLISHER = "https://publisher.walrus-testnet.walrus.space";
|
|
1030
|
+
var DEFAULT_AGGREGATOR = "https://aggregator.walrus-testnet.walrus.space";
|
|
1031
|
+
function walrusClient(opts = {}) {
|
|
1032
|
+
const publisher = (opts.publisher ?? DEFAULT_PUBLISHER).replace(/\/$/, "");
|
|
1033
|
+
const aggregator = (opts.aggregator ?? DEFAULT_AGGREGATOR).replace(/\/$/, "");
|
|
1034
|
+
const epochs = opts.epochs ?? 5;
|
|
1035
|
+
const network = opts.network ?? "testnet";
|
|
1036
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
1037
|
+
return {
|
|
1038
|
+
publisher,
|
|
1039
|
+
aggregator,
|
|
1040
|
+
network,
|
|
1041
|
+
async putBlob(data) {
|
|
1042
|
+
const body = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
1043
|
+
const res = await fetchImpl(`${publisher}/v1/blobs?epochs=${String(epochs)}`, {
|
|
1044
|
+
method: "PUT",
|
|
1045
|
+
body
|
|
1046
|
+
});
|
|
1047
|
+
if (!res.ok) {
|
|
1048
|
+
throw new Error(`walrus putBlob failed: HTTP ${String(res.status)} ${await res.text()}`);
|
|
1049
|
+
}
|
|
1050
|
+
const json = await res.json();
|
|
1051
|
+
const created = json.newlyCreated?.blobObject;
|
|
1052
|
+
const certified = json.alreadyCertified;
|
|
1053
|
+
const blobId = created?.blobId ?? certified?.blobId;
|
|
1054
|
+
if (!blobId) {
|
|
1055
|
+
throw new Error(`walrus putBlob: no blobId in response: ${JSON.stringify(json)}`);
|
|
1056
|
+
}
|
|
1057
|
+
return {
|
|
1058
|
+
blobId,
|
|
1059
|
+
objectId: created?.id,
|
|
1060
|
+
txDigest: certified?.event?.txDigest,
|
|
1061
|
+
endEpoch: created?.storage?.endEpoch ?? certified?.endEpoch
|
|
1062
|
+
};
|
|
1063
|
+
},
|
|
1064
|
+
async getBlob(blobId) {
|
|
1065
|
+
const res = await fetchImpl(`${aggregator}/v1/blobs/${encodeURIComponent(blobId)}`);
|
|
1066
|
+
if (!res.ok) {
|
|
1067
|
+
throw new Error(`walrus getBlob failed: HTTP ${String(res.status)} for ${blobId}`);
|
|
1068
|
+
}
|
|
1069
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
function walruscanBlobUrl(blobId, network = "testnet") {
|
|
1074
|
+
return `https://walruscan.com/${network}/blob/${blobId}`;
|
|
1075
|
+
}
|
|
1076
|
+
function suiscanObjectUrl(objectId, network = "testnet") {
|
|
1077
|
+
return `https://suiscan.xyz/${network}/object/${objectId}`;
|
|
1078
|
+
}
|
|
1079
|
+
function suiscanTxUrl(txDigest, network = "testnet") {
|
|
1080
|
+
return `https://suiscan.xyz/${network}/tx/${txDigest}`;
|
|
1081
|
+
}
|
|
1082
|
+
function explorerLinks(ref, network = "testnet") {
|
|
1083
|
+
return {
|
|
1084
|
+
blob: walruscanBlobUrl(ref.blobId, network),
|
|
1085
|
+
object: ref.objectId ? suiscanObjectUrl(ref.objectId, network) : void 0,
|
|
1086
|
+
tx: ref.txDigest ? suiscanTxUrl(ref.txDigest, network) : void 0
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
function walrusAuditLogger(base, client, opts) {
|
|
1090
|
+
const buffer = [];
|
|
1091
|
+
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
1092
|
+
const capture = (level, obj, msg) => {
|
|
1093
|
+
if ("kind" in obj || "event" in obj) {
|
|
1094
|
+
buffer.push({ level, at: now().toISOString(), msg, ...obj });
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
const wrap = (logger) => ({
|
|
1098
|
+
info: (obj, msg) => {
|
|
1099
|
+
capture("info", obj, msg);
|
|
1100
|
+
logger.info(obj, msg);
|
|
1101
|
+
},
|
|
1102
|
+
warn: (obj, msg) => {
|
|
1103
|
+
capture("warn", obj, msg);
|
|
1104
|
+
logger.warn(obj, msg);
|
|
1105
|
+
},
|
|
1106
|
+
error: (obj, msg) => {
|
|
1107
|
+
capture("error", obj, msg);
|
|
1108
|
+
logger.error(obj, msg);
|
|
1109
|
+
},
|
|
1110
|
+
// child loggers share the same buffer so the whole session lands in one trail.
|
|
1111
|
+
child: (bindings) => wrap(logger.child(bindings)),
|
|
1112
|
+
entries: () => [...buffer],
|
|
1113
|
+
reset: () => {
|
|
1114
|
+
buffer.length = 0;
|
|
1115
|
+
},
|
|
1116
|
+
flush: async (sessionId) => {
|
|
1117
|
+
if (buffer.length === 0) return null;
|
|
1118
|
+
const manifest = {
|
|
1119
|
+
sessionId: sessionId ?? opts.sessionId,
|
|
1120
|
+
createdAt: now().toISOString(),
|
|
1121
|
+
count: buffer.length,
|
|
1122
|
+
entries: buffer
|
|
1123
|
+
};
|
|
1124
|
+
return client.putBlob(JSON.stringify(manifest));
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1127
|
+
return wrap(base);
|
|
1128
|
+
}
|
|
1129
|
+
async function verifyAuditTrail(client, blobId) {
|
|
1130
|
+
const bytes = await client.getBlob(blobId);
|
|
1131
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
1132
|
+
}
|
|
1133
|
+
function filePointerStore(path) {
|
|
1134
|
+
async function readAll() {
|
|
1135
|
+
try {
|
|
1136
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
1137
|
+
} catch {
|
|
1138
|
+
return {};
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
return {
|
|
1142
|
+
async get(key) {
|
|
1143
|
+
return (await readAll())[key];
|
|
1144
|
+
},
|
|
1145
|
+
async set(key, blobId) {
|
|
1146
|
+
const all = await readAll();
|
|
1147
|
+
all[key] = blobId;
|
|
1148
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1149
|
+
await writeFile(path, JSON.stringify(all, null, 2));
|
|
1150
|
+
}
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
function emptyFacts(userId) {
|
|
1154
|
+
return { userId, facts: [], preferences: [], updatedAt: "" };
|
|
1155
|
+
}
|
|
1156
|
+
function walrusMemoryPlugin(opts) {
|
|
1157
|
+
const key = `facts:${opts.userId}`;
|
|
1158
|
+
const maxFacts = opts.maxFacts ?? 50;
|
|
1159
|
+
let cache;
|
|
1160
|
+
let loading;
|
|
1161
|
+
let pending = Promise.resolve();
|
|
1162
|
+
async function load() {
|
|
1163
|
+
if (cache) return cache;
|
|
1164
|
+
loading ??= (async () => {
|
|
1165
|
+
try {
|
|
1166
|
+
const blobId = await opts.pointers.get(key);
|
|
1167
|
+
cache = blobId === void 0 ? emptyFacts(opts.userId) : JSON.parse(
|
|
1168
|
+
new TextDecoder().decode(await opts.client.getBlob(blobId))
|
|
1169
|
+
);
|
|
1170
|
+
} catch {
|
|
1171
|
+
cache = emptyFacts(opts.userId);
|
|
1172
|
+
}
|
|
1173
|
+
return cache;
|
|
1174
|
+
})();
|
|
1175
|
+
return loading;
|
|
1176
|
+
}
|
|
1177
|
+
function save(facts) {
|
|
1178
|
+
cache = facts;
|
|
1179
|
+
opts.onStoreStart?.();
|
|
1180
|
+
pending = pending.then(async () => {
|
|
1181
|
+
try {
|
|
1182
|
+
const ref = await opts.client.putBlob(JSON.stringify(facts));
|
|
1183
|
+
await opts.pointers.set(key, ref.blobId);
|
|
1184
|
+
opts.onStore?.(ref);
|
|
1185
|
+
} catch {
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
void load();
|
|
1190
|
+
const contextMiddleware = async (req, next) => {
|
|
1191
|
+
const mem = await load();
|
|
1192
|
+
if (mem.facts.length === 0 && mem.preferences.length === 0) return next(req);
|
|
1193
|
+
const parts = [`[User Memory for ${mem.userId}] (durable, stored on Walrus)`];
|
|
1194
|
+
if (mem.facts.length > 0)
|
|
1195
|
+
parts.push(`Known facts:
|
|
1196
|
+
${mem.facts.map((f) => `- ${f}`).join("\n")}`);
|
|
1197
|
+
if (mem.preferences.length > 0)
|
|
1198
|
+
parts.push(`Preferences:
|
|
1199
|
+
${mem.preferences.map((p2) => `- ${p2}`).join("\n")}`);
|
|
1200
|
+
const ctxMsg = { role: "system", content: parts.join("\n\n") };
|
|
1201
|
+
const [first, ...rest] = req.messages;
|
|
1202
|
+
const messages = first?.role === "system" && first.content.includes("MUST always refer") ? [first, ctxMsg, ...rest] : [ctxMsg, ...req.messages];
|
|
1203
|
+
return next({ ...req, messages });
|
|
1204
|
+
};
|
|
1205
|
+
return {
|
|
1206
|
+
name: "walrus-memory",
|
|
1207
|
+
// Await any in-flight background write — call before exit for last-write durability.
|
|
1208
|
+
flush: () => pending,
|
|
1209
|
+
modelMiddleware: [contextMiddleware],
|
|
1210
|
+
tools: [
|
|
1211
|
+
defineTool({
|
|
1212
|
+
name: "remember_fact",
|
|
1213
|
+
description: "Store a durable fact or preference about the user on Walrus, to recall in future sessions. Use whenever the user shares something durable \u2014 their name, role, preferences, projects, or goals.",
|
|
1214
|
+
parameters: z2.object({
|
|
1215
|
+
fact: z2.string().min(1).describe("The fact or preference, phrased to stand on its own."),
|
|
1216
|
+
kind: z2.enum(["fact", "preference"]).default("fact")
|
|
1217
|
+
}),
|
|
1218
|
+
execute: async ({ fact, kind }) => {
|
|
1219
|
+
const mem = await load();
|
|
1220
|
+
const list = kind === "preference" ? mem.preferences : mem.facts;
|
|
1221
|
+
if (!list.includes(fact)) {
|
|
1222
|
+
list.push(fact);
|
|
1223
|
+
if (list.length > maxFacts) list.shift();
|
|
1224
|
+
}
|
|
1225
|
+
mem.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1226
|
+
save(mem);
|
|
1227
|
+
return {
|
|
1228
|
+
stored: fact,
|
|
1229
|
+
kind,
|
|
1230
|
+
totalFacts: mem.facts.length,
|
|
1231
|
+
totalPreferences: mem.preferences.length
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
}),
|
|
1235
|
+
defineTool({
|
|
1236
|
+
name: "recall_memory",
|
|
1237
|
+
description: "Retrieve everything currently remembered about the user (facts + preferences). Use when asked what you remember.",
|
|
1238
|
+
parameters: z2.object({}),
|
|
1239
|
+
execute: () => load()
|
|
1240
|
+
})
|
|
1241
|
+
]
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// ../../packages/plugins/agents/src/index.ts
|
|
1246
|
+
import { z as z3 } from "zod";
|
|
1247
|
+
var DEFAULT_SUBAGENT_PROMPT = "You are a focused sub-agent. Complete the single delegated task using the tools available, then return only the result \u2014 concise and self-contained. Do not ask follow-up questions.";
|
|
1248
|
+
var PLAN_STATE_KEY = "thiny:agent-plan";
|
|
1249
|
+
function agentsPlugin(opts = {}) {
|
|
1250
|
+
const subagents = opts.subagents ?? {};
|
|
1251
|
+
const names = Object.keys(subagents);
|
|
1252
|
+
const agentParamDesc = names.length > 0 ? `Optional named sub-agent to use. Available: ${names.map((n) => `"${n}" (${subagents[n]?.description ?? ""})`).join("; ")}. Omit for a general-purpose sub-agent.` : "Optional named sub-agent. None are configured, so omit it for a general-purpose sub-agent.";
|
|
1253
|
+
const delegateTask = defineTool({
|
|
1254
|
+
name: "delegate_task",
|
|
1255
|
+
description: "Delegate a focused, self-contained task to a scoped sub-agent that runs independently and returns its result. Use for parallelisable or specialised work (research, summarisation, a sub-problem) so the main thread stays clean. The sub-agent does NOT see this conversation \u2014 put everything it needs in `task` and `context`.",
|
|
1256
|
+
parameters: z3.object({
|
|
1257
|
+
task: z3.string().min(1).describe("The complete instruction for the sub-agent."),
|
|
1258
|
+
agent: z3.string().optional().describe(agentParamDesc),
|
|
1259
|
+
context: z3.string().optional().describe("Any background the sub-agent needs (it cannot see the current conversation).")
|
|
1260
|
+
}),
|
|
1261
|
+
execute: async ({ task, agent, context }, ctx) => {
|
|
1262
|
+
if (!ctx.spawn) {
|
|
1263
|
+
throw new Error("delegate_task: sub-agent spawning is unavailable in this context.");
|
|
1264
|
+
}
|
|
1265
|
+
let def;
|
|
1266
|
+
if (agent !== void 0) {
|
|
1267
|
+
def = subagents[agent];
|
|
1268
|
+
if (!def) {
|
|
1269
|
+
throw new Error(
|
|
1270
|
+
`delegate_task: unknown sub-agent "${agent}". Available: ${names.length > 0 ? names.join(", ") : "(none configured)"}.`
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
const input = context ? `${task}
|
|
1275
|
+
|
|
1276
|
+
--- Context ---
|
|
1277
|
+
${context}` : task;
|
|
1278
|
+
ctx.logger.info(
|
|
1279
|
+
{ event: "delegate_task", agent: agent ?? "default", taskLength: task.length },
|
|
1280
|
+
`Delegating task to ${agent ?? "default"} sub-agent`
|
|
1281
|
+
);
|
|
1282
|
+
const result = await ctx.spawn({
|
|
1283
|
+
input,
|
|
1284
|
+
systemPrompt: def?.systemPrompt ?? DEFAULT_SUBAGENT_PROMPT,
|
|
1285
|
+
tools: def?.tools ?? opts.defaultTools,
|
|
1286
|
+
maxSteps: opts.maxSteps
|
|
1287
|
+
});
|
|
1288
|
+
return { agent: agent ?? "default", result };
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1291
|
+
const updatePlan = defineTool({
|
|
1292
|
+
name: "update_plan",
|
|
1293
|
+
description: "Record or update your task plan as a checklist. Call this when you start a multi-step task and whenever a step's status changes. Pass the FULL list each time (it replaces the previous plan). Keeps progress visible and helps you stay on track.",
|
|
1294
|
+
parameters: z3.object({
|
|
1295
|
+
steps: z3.array(
|
|
1296
|
+
z3.object({
|
|
1297
|
+
step: z3.string().min(1).describe("A concise description of the step."),
|
|
1298
|
+
status: z3.enum(["pending", "in_progress", "done"]).describe("Current status of this step.")
|
|
1299
|
+
})
|
|
1300
|
+
).min(1).describe("The full ordered plan \u2014 replaces the previous list.")
|
|
1301
|
+
}),
|
|
1302
|
+
execute: ({ steps }, ctx) => {
|
|
1303
|
+
ctx.state.set(PLAN_STATE_KEY, steps);
|
|
1304
|
+
ctx.logger.info({ event: "plan_update", steps }, "Plan updated");
|
|
1305
|
+
const done = steps.filter((s) => s.status === "done").length;
|
|
1306
|
+
return Promise.resolve({
|
|
1307
|
+
plan: steps,
|
|
1308
|
+
summary: `${String(done)}/${String(steps.length)} done`
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
});
|
|
1312
|
+
return { name: "agents", tools: [delegateTask, updatePlan] };
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// ../../packages/skills/src/catalog.ts
|
|
1316
|
+
var BUILTIN_SKILLS = [
|
|
1317
|
+
{
|
|
1318
|
+
id: "web-search",
|
|
1319
|
+
name: "Web Search",
|
|
1320
|
+
description: "Search the public web via Brave Search API",
|
|
1321
|
+
category: "web",
|
|
1322
|
+
tags: ["search", "web"],
|
|
1323
|
+
requiredEnv: ["BRAVE_API_KEY"]
|
|
1324
|
+
},
|
|
1325
|
+
{
|
|
1326
|
+
id: "evm",
|
|
1327
|
+
name: "EVM / Ethereum",
|
|
1328
|
+
description: "Read EVM chain state, testnet sends with policy gate",
|
|
1329
|
+
category: "defi",
|
|
1330
|
+
tags: ["ethereum", "evm", "web3"],
|
|
1331
|
+
requiredEnv: ["EVM_RPC_URL"]
|
|
1332
|
+
},
|
|
1333
|
+
{
|
|
1334
|
+
id: "solana",
|
|
1335
|
+
name: "Solana",
|
|
1336
|
+
description: "Read Solana devnet state, devnet SOL sends",
|
|
1337
|
+
category: "defi",
|
|
1338
|
+
tags: ["solana", "sol", "web3"]
|
|
1339
|
+
},
|
|
1340
|
+
{
|
|
1341
|
+
id: "market",
|
|
1342
|
+
name: "Market Data",
|
|
1343
|
+
description: "Token prices via CoinGecko + in-run portfolio tracking",
|
|
1344
|
+
category: "defi",
|
|
1345
|
+
tags: ["prices", "portfolio", "defi"]
|
|
1346
|
+
},
|
|
1347
|
+
{
|
|
1348
|
+
id: "tokens",
|
|
1349
|
+
name: "Token Ops",
|
|
1350
|
+
description: "ERC-20 balance, allowance, approve (unlimited blocked), transfer",
|
|
1351
|
+
category: "defi",
|
|
1352
|
+
tags: ["erc20", "tokens", "approve"],
|
|
1353
|
+
requiredEnv: ["EVM_RPC_URL"]
|
|
1354
|
+
},
|
|
1355
|
+
{
|
|
1356
|
+
id: "trading-policy",
|
|
1357
|
+
name: "Trading Policy",
|
|
1358
|
+
description: "Asset allowlist + position size cap + slippage ceiling",
|
|
1359
|
+
category: "defi",
|
|
1360
|
+
tags: ["trading", "policy", "safety"],
|
|
1361
|
+
requiredEnv: ["ALLOWED_ASSETS"]
|
|
1362
|
+
},
|
|
1363
|
+
{
|
|
1364
|
+
id: "knowledge",
|
|
1365
|
+
name: "Knowledge / RAG",
|
|
1366
|
+
description: "Ingest documents, auto-inject relevant context",
|
|
1367
|
+
category: "ai",
|
|
1368
|
+
tags: ["rag", "knowledge", "retrieval"]
|
|
1369
|
+
},
|
|
1370
|
+
{
|
|
1371
|
+
id: "resilience",
|
|
1372
|
+
name: "Resilience",
|
|
1373
|
+
description: "retry, timeout, rate-limit, cache, idempotency middleware",
|
|
1374
|
+
category: "reliability",
|
|
1375
|
+
tags: ["retry", "timeout", "cache"]
|
|
1376
|
+
},
|
|
1377
|
+
{
|
|
1378
|
+
id: "mcp",
|
|
1379
|
+
name: "MCP",
|
|
1380
|
+
description: "Connect to any MCP stdio server as instant tools",
|
|
1381
|
+
category: "ecosystem",
|
|
1382
|
+
tags: ["mcp", "integration"],
|
|
1383
|
+
requiredEnv: ["MCP_COMMAND"]
|
|
1384
|
+
},
|
|
1385
|
+
{
|
|
1386
|
+
id: "agent-skills",
|
|
1387
|
+
name: "Agent Skills",
|
|
1388
|
+
description: "Understand, find, install, and create skills.sh compatible skills",
|
|
1389
|
+
category: "ecosystem",
|
|
1390
|
+
tags: ["skills", "skills.sh", "community", "find-skills"]
|
|
1391
|
+
}
|
|
1392
|
+
];
|
|
1393
|
+
|
|
1394
|
+
// ../../packages/skills/src/registry.ts
|
|
1395
|
+
var SkillRegistry = class {
|
|
1396
|
+
skills = /* @__PURE__ */ new Map();
|
|
1397
|
+
constructor(definitions = BUILTIN_SKILLS) {
|
|
1398
|
+
for (const def of definitions) this.skills.set(def.id, def);
|
|
1399
|
+
}
|
|
1400
|
+
/** Register a custom skill. */
|
|
1401
|
+
add(definition) {
|
|
1402
|
+
this.skills.set(definition.id, definition);
|
|
1403
|
+
}
|
|
1404
|
+
/** Look up a skill by ID. Returns `undefined` when not registered. */
|
|
1405
|
+
get(id) {
|
|
1406
|
+
return this.skills.get(id);
|
|
1407
|
+
}
|
|
1408
|
+
/** All registered skills, sorted by category then id. */
|
|
1409
|
+
all() {
|
|
1410
|
+
return [...this.skills.values()].sort(
|
|
1411
|
+
(a, b) => a.category === b.category ? a.id.localeCompare(b.id) : a.category.localeCompare(b.category)
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
/** Skills grouped by category for the TUI display panel. */
|
|
1415
|
+
byCategory() {
|
|
1416
|
+
const map = /* @__PURE__ */ new Map();
|
|
1417
|
+
for (const skill of this.all()) {
|
|
1418
|
+
const list = map.get(skill.category) ?? [];
|
|
1419
|
+
list.push(skill);
|
|
1420
|
+
map.set(skill.category, list);
|
|
1421
|
+
}
|
|
1422
|
+
return map;
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Check which skill IDs are satisfiable given the current environment.
|
|
1426
|
+
* Returns the IDs that are ready to load and a list of warnings for the rest.
|
|
1427
|
+
*/
|
|
1428
|
+
checkEnv(ids, env = process.env) {
|
|
1429
|
+
const satisfied = [];
|
|
1430
|
+
const warnings = [];
|
|
1431
|
+
for (const id of ids) {
|
|
1432
|
+
const def = this.skills.get(id);
|
|
1433
|
+
if (!def) {
|
|
1434
|
+
warnings.push(`Unknown skill: "${id}". Run /skills to see available skills.`);
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
const missing = (def.requiredEnv ?? []).filter((k) => !env[k]);
|
|
1438
|
+
if (missing.length > 0) {
|
|
1439
|
+
warnings.push(`Skill "${id}" needs: ${missing.join(", ")} \u2014 skipping.`);
|
|
1440
|
+
continue;
|
|
1441
|
+
}
|
|
1442
|
+
satisfied.push(id);
|
|
1443
|
+
}
|
|
1444
|
+
return { satisfied, warnings };
|
|
1445
|
+
}
|
|
1446
|
+
};
|
|
1447
|
+
|
|
1448
|
+
// ../../packages/skills/src/index.ts
|
|
1449
|
+
var defaultRegistry = new SkillRegistry();
|
|
1450
|
+
|
|
1451
|
+
// src/skills.ts
|
|
1452
|
+
async function loadSkills(ids, env = process.env) {
|
|
1453
|
+
const { satisfied, warnings } = defaultRegistry.checkEnv(ids, env);
|
|
1454
|
+
const plugins = [];
|
|
1455
|
+
for (const id of satisfied) {
|
|
1456
|
+
try {
|
|
1457
|
+
const plugin = await createSkillPlugin(id, env);
|
|
1458
|
+
if (Array.isArray(plugin)) {
|
|
1459
|
+
plugins.push(...plugin);
|
|
1460
|
+
} else if (plugin) {
|
|
1461
|
+
plugins.push(plugin);
|
|
1462
|
+
}
|
|
1463
|
+
} catch (err) {
|
|
1464
|
+
warnings.push(
|
|
1465
|
+
`Failed to load skill "${id}": ${err instanceof Error ? err.message : String(err)}`
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
return { plugins, warnings };
|
|
1470
|
+
}
|
|
1471
|
+
var BUILTIN_PACKAGES = {
|
|
1472
|
+
"web-search": "@thiny/plugin-web-search",
|
|
1473
|
+
evm: "@thiny/plugin-evm",
|
|
1474
|
+
solana: "@thiny/plugin-solana",
|
|
1475
|
+
market: "@thiny/plugin-market",
|
|
1476
|
+
tokens: "@thiny/plugin-tokens",
|
|
1477
|
+
"trading-policy": "@thiny/plugin-trading-policy",
|
|
1478
|
+
knowledge: "@thiny/plugin-knowledge",
|
|
1479
|
+
resilience: "@thiny/plugin-resilience",
|
|
1480
|
+
mcp: "@thiny/mcp",
|
|
1481
|
+
"agent-skills": "@thiny/plugin-agent-skills"
|
|
1482
|
+
};
|
|
1483
|
+
async function createSkillPlugin(id, env) {
|
|
1484
|
+
const packageName = BUILTIN_PACKAGES[id] ?? (id.startsWith("thiny-skill-") ? id : `thiny-skill-${id}`);
|
|
1485
|
+
try {
|
|
1486
|
+
const mod = await import(packageName);
|
|
1487
|
+
const factory = mod.default;
|
|
1488
|
+
if (typeof factory !== "function") {
|
|
1489
|
+
throw new Error(`Package "${packageName}" does not export a default factory function.`);
|
|
1490
|
+
}
|
|
1491
|
+
return await factory(env);
|
|
1492
|
+
} catch (err) {
|
|
1493
|
+
if (BUILTIN_PACKAGES[id]) {
|
|
1494
|
+
throw err;
|
|
1495
|
+
}
|
|
1496
|
+
return null;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// src/ui.ts
|
|
1501
|
+
import figlet from "figlet";
|
|
1502
|
+
import chalk from "chalk";
|
|
1503
|
+
var BRAND = chalk.cyan;
|
|
1504
|
+
var DIM = chalk.dim;
|
|
1505
|
+
var AGENT_LABEL = BRAND.bold;
|
|
1506
|
+
var ERROR_COLOR = chalk.red;
|
|
1507
|
+
var SUCCESS_COLOR = chalk.green;
|
|
1508
|
+
function getWidth() {
|
|
1509
|
+
return process.stdout.columns || 80;
|
|
1510
|
+
}
|
|
1511
|
+
var ANSI_REGEX = /\x1B\[[0-9;]*m/g;
|
|
1512
|
+
var visibleLen = (s) => s.replace(ANSI_REGEX, "").length;
|
|
1513
|
+
function padRight(str, len) {
|
|
1514
|
+
return str + " ".repeat(Math.max(0, len - visibleLen(str)));
|
|
1515
|
+
}
|
|
1516
|
+
function center(str, width) {
|
|
1517
|
+
const pad = Math.max(0, Math.floor((width - visibleLen(str)) / 2));
|
|
1518
|
+
return " ".repeat(pad) + str;
|
|
1519
|
+
}
|
|
1520
|
+
function renderHeader(opts) {
|
|
1521
|
+
const w = getWidth();
|
|
1522
|
+
let title;
|
|
1523
|
+
try {
|
|
1524
|
+
title = figlet.textSync(opts.persona ?? "Thiny", { font: "Standard" });
|
|
1525
|
+
} catch {
|
|
1526
|
+
title = opts.persona ?? "Thiny";
|
|
1527
|
+
}
|
|
1528
|
+
process.stdout.write("\n");
|
|
1529
|
+
for (const line of title.split("\n")) {
|
|
1530
|
+
if (line.trim()) process.stdout.write(center(BRAND.bold(line), w) + "\n");
|
|
1531
|
+
}
|
|
1532
|
+
process.stdout.write("\n");
|
|
1533
|
+
const version2 = opts.version ?? "v0.1.0";
|
|
1534
|
+
const infoText = ` ${opts.persona ?? "Thiny"} Agent ${version2} `;
|
|
1535
|
+
const remaining = Math.max(0, w - visibleLen(infoText));
|
|
1536
|
+
const leftDash = "\u2500".repeat(Math.floor(remaining / 2));
|
|
1537
|
+
const rightDash = "\u2500".repeat(remaining - leftDash.length);
|
|
1538
|
+
process.stdout.write(BRAND(leftDash) + BRAND.bold(infoText) + BRAND(rightDash) + "\n\n");
|
|
1539
|
+
}
|
|
1540
|
+
function renderToolsAndSkills(tools, skills, opts) {
|
|
1541
|
+
const w = getWidth();
|
|
1542
|
+
const leftColW = 25;
|
|
1543
|
+
const leftLines = [
|
|
1544
|
+
"",
|
|
1545
|
+
center(BRAND.bold(opts.persona ?? "Thiny"), leftColW),
|
|
1546
|
+
center(DIM(opts.model.slice(0, leftColW - 2)), leftColW),
|
|
1547
|
+
center(DIM(`Session: ${opts.session.slice(-8)}`), leftColW)
|
|
1548
|
+
];
|
|
1549
|
+
const toolGroups = /* @__PURE__ */ new Map();
|
|
1550
|
+
for (const tool of tools) {
|
|
1551
|
+
const idx = tool.indexOf("_");
|
|
1552
|
+
const prefix = idx !== -1 ? tool.slice(0, idx) : "core";
|
|
1553
|
+
const list = toolGroups.get(prefix) ?? [];
|
|
1554
|
+
list.push(tool);
|
|
1555
|
+
toolGroups.set(prefix, list);
|
|
1556
|
+
}
|
|
1557
|
+
const rightLines = [];
|
|
1558
|
+
rightLines.push(BRAND.bold("Available Tools"));
|
|
1559
|
+
for (const [prefix, names] of toolGroups) {
|
|
1560
|
+
rightLines.push(` ${BRAND(prefix)}: ${names.join(", ")}`);
|
|
1561
|
+
}
|
|
1562
|
+
rightLines.push("");
|
|
1563
|
+
rightLines.push(BRAND.bold("Available Skills"));
|
|
1564
|
+
if (skills.size === 0) {
|
|
1565
|
+
rightLines.push(` ${DIM("(none loaded \u2014 use --skills <id>)")}`);
|
|
1566
|
+
} else {
|
|
1567
|
+
for (const [cat, names] of skills) {
|
|
1568
|
+
rightLines.push(` ${BRAND(cat)}: ${names.join(", ")}`);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
const maxLines = Math.max(leftLines.length, rightLines.length);
|
|
1572
|
+
process.stdout.write(BRAND("\u250C" + "\u2500".repeat(w - 2) + "\u2510") + "\n");
|
|
1573
|
+
for (let i = 0; i < maxLines; i++) {
|
|
1574
|
+
const leftRaw = leftLines[i] ?? "";
|
|
1575
|
+
const rightRaw = rightLines[i] ?? "";
|
|
1576
|
+
const leftPad = padRight(leftRaw, leftColW);
|
|
1577
|
+
const spacer = BRAND(" \u2502 ");
|
|
1578
|
+
const rightColW = w - leftColW - 7;
|
|
1579
|
+
const rightPad = padRight(rightRaw, rightColW);
|
|
1580
|
+
process.stdout.write(BRAND("\u2502 ") + leftPad + spacer + rightPad + BRAND(" \u2502") + "\n");
|
|
1581
|
+
}
|
|
1582
|
+
process.stdout.write(BRAND("\u2514" + "\u2500".repeat(w - 2) + "\u2518") + "\n");
|
|
1583
|
+
}
|
|
1584
|
+
function renderHints(logFile) {
|
|
1585
|
+
const logHint = logFile ? ` \xB7 ${DIM("logs \u2192")} ${chalk.dim(logFile)}` : "";
|
|
1586
|
+
process.stdout.write(
|
|
1587
|
+
"\n" + DIM("Type a message \xB7 ") + DIM("/new") + chalk.dim(" new session \xB7 ") + DIM("/skills") + chalk.dim(" list skills \xB7 ") + DIM("/tools") + chalk.dim(" list tools \xB7 ") + DIM("Ctrl+C") + chalk.dim(" quit") + logHint + "\n\n"
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
function renderAgentLabel(name) {
|
|
1591
|
+
process.stdout.write("\n" + AGENT_LABEL(name) + "\n");
|
|
1592
|
+
}
|
|
1593
|
+
function renderAgentDone() {
|
|
1594
|
+
process.stdout.write("\n");
|
|
1595
|
+
}
|
|
1596
|
+
function renderError(message) {
|
|
1597
|
+
process.stdout.write("\n" + ERROR_COLOR("Error: ") + chalk.white(message) + "\n");
|
|
1598
|
+
}
|
|
1599
|
+
function renderInfo(message) {
|
|
1600
|
+
process.stdout.write(DIM(message) + "\n");
|
|
1601
|
+
}
|
|
1602
|
+
function renderWarning(message) {
|
|
1603
|
+
process.stdout.write(chalk.yellow("\u26A0 ") + chalk.white(message) + "\n");
|
|
1604
|
+
}
|
|
1605
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1606
|
+
var Spinner = class {
|
|
1607
|
+
intervalId = null;
|
|
1608
|
+
frameIdx = 0;
|
|
1609
|
+
start(label) {
|
|
1610
|
+
this.stop();
|
|
1611
|
+
process.stdout.write("\n");
|
|
1612
|
+
this.intervalId = setInterval(() => {
|
|
1613
|
+
const frame = SPINNER_FRAMES[this.frameIdx % SPINNER_FRAMES.length] ?? "\u280B";
|
|
1614
|
+
process.stdout.write(`\r ${BRAND(frame)} ${DIM(label)} `);
|
|
1615
|
+
this.frameIdx++;
|
|
1616
|
+
}, 80);
|
|
1617
|
+
}
|
|
1618
|
+
stop() {
|
|
1619
|
+
if (this.intervalId) {
|
|
1620
|
+
clearInterval(this.intervalId);
|
|
1621
|
+
this.intervalId = null;
|
|
1622
|
+
process.stdout.write("\r" + " ".repeat(40) + "\r");
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
};
|
|
1626
|
+
function clearScreen() {
|
|
1627
|
+
process.stdout.write("\x1Bc");
|
|
1628
|
+
}
|
|
1629
|
+
function formatTokens(n) {
|
|
1630
|
+
return n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : String(n);
|
|
1631
|
+
}
|
|
1632
|
+
function renderStatus(parts) {
|
|
1633
|
+
const shown = parts.filter((p2) => p2.length > 0);
|
|
1634
|
+
if (shown.length === 0) return;
|
|
1635
|
+
process.stdout.write(DIM(` ${shown.join(" \xB7 ")}`) + "\n");
|
|
1636
|
+
}
|
|
1637
|
+
function renderStored(label, links, backend = "Walrus") {
|
|
1638
|
+
process.stdout.write(
|
|
1639
|
+
SUCCESS_COLOR(" \u2713 ") + DIM(`${label} on ${backend} \xB7 `) + chalk.dim.underline(links.blob) + "\n"
|
|
1640
|
+
);
|
|
1641
|
+
}
|
|
1642
|
+
function renderSaving(label, backend = "Walrus") {
|
|
1643
|
+
process.stdout.write(DIM(` \u27F3 saving ${label} to ${backend}\u2026`) + "\n");
|
|
1644
|
+
}
|
|
1645
|
+
var THINK_OPEN = "<think>";
|
|
1646
|
+
var THINK_CLOSE = "</think>";
|
|
1647
|
+
function pendingTagLen(s) {
|
|
1648
|
+
let max = 0;
|
|
1649
|
+
for (const tag of [THINK_OPEN, THINK_CLOSE]) {
|
|
1650
|
+
for (let n = Math.min(s.length, tag.length - 1); n > 0; n--) {
|
|
1651
|
+
if (s.endsWith(tag.slice(0, n))) {
|
|
1652
|
+
if (n > max) max = n;
|
|
1653
|
+
break;
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
return max;
|
|
1658
|
+
}
|
|
1659
|
+
var OSC8 = (url, text2) => `\x1B]8;;${url}\x07${text2}\x1B]8;;\x07`;
|
|
1660
|
+
function renderInline(s) {
|
|
1661
|
+
const codes = [];
|
|
1662
|
+
s = s.replace(/`([^`]+)`/g, (_m, c) => {
|
|
1663
|
+
codes.push(c);
|
|
1664
|
+
return `\0${String(codes.length - 1)}\0`;
|
|
1665
|
+
});
|
|
1666
|
+
s = s.replace(/\*\*([^*\n]+)\*\*/g, (_m, t) => chalk.bold(t));
|
|
1667
|
+
s = s.replace(/\*([^*\n]+)\*/g, (_m, t) => chalk.italic(t));
|
|
1668
|
+
s = s.replace(/~~([^~\n]+)~~/g, (_m, t) => chalk.strikethrough(t));
|
|
1669
|
+
s = s.replace(
|
|
1670
|
+
/\[([^\]]+)\]\(([^)\s]+)\)/g,
|
|
1671
|
+
(_m, text2, url) => OSC8(url, chalk.cyan.underline(text2))
|
|
1672
|
+
);
|
|
1673
|
+
s = s.replace(/(\d+)/g, (_m, i) => chalk.cyan(codes[Number(i)] ?? ""));
|
|
1674
|
+
return s;
|
|
1675
|
+
}
|
|
1676
|
+
function renderMarkdownLine(line, inCode, setCode) {
|
|
1677
|
+
const trimmed = line.replace(/^\s+/, "");
|
|
1678
|
+
const indent = line.slice(0, line.length - trimmed.length);
|
|
1679
|
+
if (trimmed.startsWith("```")) {
|
|
1680
|
+
setCode(!inCode);
|
|
1681
|
+
return "";
|
|
1682
|
+
}
|
|
1683
|
+
if (inCode) return chalk.cyan(line);
|
|
1684
|
+
const h = /^(#{1,6})\s+(.*)$/.exec(trimmed);
|
|
1685
|
+
if (h) return ((h[1] ?? "").length <= 2 ? chalk.bold.underline : chalk.bold)(renderInline(h[2] ?? ""));
|
|
1686
|
+
if (/^(-{3,}|\*{3,}|_{3,})$/.test(trimmed)) return chalk.dim("\u2500".repeat(Math.min(getWidth(), 50)));
|
|
1687
|
+
const q = /^>\s?(.*)$/.exec(trimmed);
|
|
1688
|
+
if (q) return chalk.dim(`\u2502 ${renderInline(q[1] ?? "")}`);
|
|
1689
|
+
const b = /^[-*+]\s+(.*)$/.exec(trimmed);
|
|
1690
|
+
if (b) return `${indent}${chalk.cyan("\u2022")} ${renderInline(b[1] ?? "")}`;
|
|
1691
|
+
const n = /^(\d+)[.)]\s+(.*)$/.exec(trimmed);
|
|
1692
|
+
if (n) return `${indent}${chalk.cyan(`${n[1] ?? ""}.`)} ${renderInline(n[2] ?? "")}`;
|
|
1693
|
+
return renderInline(line);
|
|
1694
|
+
}
|
|
1695
|
+
function createMarkdownWriter(write) {
|
|
1696
|
+
let inThink = false;
|
|
1697
|
+
let inCode = false;
|
|
1698
|
+
let buf = "";
|
|
1699
|
+
let line = "";
|
|
1700
|
+
const flushLine = (newline) => {
|
|
1701
|
+
write(renderMarkdownLine(line, inCode, (v) => inCode = v) + (newline ? "\n" : ""));
|
|
1702
|
+
line = "";
|
|
1703
|
+
};
|
|
1704
|
+
const emit = (text2, think) => {
|
|
1705
|
+
if (!text2) return;
|
|
1706
|
+
if (think) {
|
|
1707
|
+
write(chalk.dim.italic(text2));
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
for (const ch of text2) {
|
|
1711
|
+
if (ch === "\n") flushLine(true);
|
|
1712
|
+
else line += ch;
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
const drain = () => {
|
|
1716
|
+
for (; ; ) {
|
|
1717
|
+
const open = buf.indexOf(THINK_OPEN);
|
|
1718
|
+
const close = buf.indexOf(THINK_CLOSE);
|
|
1719
|
+
const present = [open, close].filter((i) => i !== -1);
|
|
1720
|
+
if (present.length === 0) break;
|
|
1721
|
+
const idx = Math.min(...present);
|
|
1722
|
+
const isOpen = idx === open;
|
|
1723
|
+
emit(buf.slice(0, idx), inThink);
|
|
1724
|
+
if (isOpen && line) flushLine(false);
|
|
1725
|
+
inThink = isOpen;
|
|
1726
|
+
buf = buf.slice(idx + (isOpen ? THINK_OPEN.length : THINK_CLOSE.length));
|
|
1727
|
+
}
|
|
1728
|
+
const hold = pendingTagLen(buf);
|
|
1729
|
+
emit(buf.slice(0, buf.length - hold), inThink);
|
|
1730
|
+
buf = buf.slice(buf.length - hold);
|
|
1731
|
+
};
|
|
1732
|
+
return {
|
|
1733
|
+
push: (delta) => {
|
|
1734
|
+
buf += delta;
|
|
1735
|
+
drain();
|
|
1736
|
+
},
|
|
1737
|
+
end: () => {
|
|
1738
|
+
emit(buf, inThink);
|
|
1739
|
+
buf = "";
|
|
1740
|
+
if (line) flushLine(false);
|
|
1741
|
+
}
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// src/main.ts
|
|
1746
|
+
function envOn(v) {
|
|
1747
|
+
return !!v && v !== "0" && v.toLowerCase() !== "false";
|
|
1748
|
+
}
|
|
1749
|
+
function resetTurn(t) {
|
|
1750
|
+
t.inputTokens = 0;
|
|
1751
|
+
t.outputTokens = 0;
|
|
1752
|
+
t.toolCalls = 0;
|
|
1753
|
+
t.modelCalls = 0;
|
|
1754
|
+
}
|
|
1755
|
+
function captureStats(base, turn) {
|
|
1756
|
+
const wrap = (l) => ({
|
|
1757
|
+
info: (obj, msg) => {
|
|
1758
|
+
if (obj.kind === "model_call") {
|
|
1759
|
+
turn.modelCalls += 1;
|
|
1760
|
+
const usage = obj.usage;
|
|
1761
|
+
if (usage !== null && typeof usage === "object") {
|
|
1762
|
+
const u = usage;
|
|
1763
|
+
if (typeof u.inputTokens === "number") turn.inputTokens += u.inputTokens;
|
|
1764
|
+
if (typeof u.outputTokens === "number") turn.outputTokens += u.outputTokens;
|
|
1765
|
+
}
|
|
1766
|
+
} else if (obj.kind === "tool_call") {
|
|
1767
|
+
turn.toolCalls += 1;
|
|
1768
|
+
}
|
|
1769
|
+
l.info(obj, msg);
|
|
1770
|
+
},
|
|
1771
|
+
warn: (obj, msg) => {
|
|
1772
|
+
l.warn(obj, msg);
|
|
1773
|
+
},
|
|
1774
|
+
error: (obj, msg) => {
|
|
1775
|
+
l.error(obj, msg);
|
|
1776
|
+
},
|
|
1777
|
+
child: (b) => wrap(l.child(b))
|
|
1778
|
+
});
|
|
1779
|
+
return wrap(base);
|
|
1780
|
+
}
|
|
1781
|
+
var echoTool = defineTool({
|
|
1782
|
+
name: "echo",
|
|
1783
|
+
description: "Echo text back verbatim. Use when asked to repeat or echo something.",
|
|
1784
|
+
parameters: z4.object({ text: z4.string().describe("the text to echo") }),
|
|
1785
|
+
execute: ({ text: text2 }) => Promise.resolve({ echoed: text2 })
|
|
1786
|
+
});
|
|
1787
|
+
function parseSkillArgs() {
|
|
1788
|
+
const args = process.argv.slice(2);
|
|
1789
|
+
const idx = args.indexOf("--skills");
|
|
1790
|
+
if (idx === -1) return [];
|
|
1791
|
+
return (args[idx + 1] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
1792
|
+
}
|
|
1793
|
+
var currentSessionId = `cli-${(/* @__PURE__ */ new Date()).getTime().toString()}`;
|
|
1794
|
+
async function runCli() {
|
|
1795
|
+
const envLogFile = process.env.THINY_LOG_FILE?.trim();
|
|
1796
|
+
const logFile = envLogFile && envLogFile.length > 0 ? envLogFile : `${process.env.HOME ?? "."}/thiny-cli.log`;
|
|
1797
|
+
const fileLogger = pinoLogger({ level: process.env.LOG_LEVEL ?? "info", file: logFile });
|
|
1798
|
+
const turn = { inputTokens: 0, outputTokens: 0, toolCalls: 0, modelCalls: 0 };
|
|
1799
|
+
const session = { inputTokens: 0, outputTokens: 0, toolCalls: 0, turns: 0 };
|
|
1800
|
+
const logger = captureStats(fileLogger, turn);
|
|
1801
|
+
const activeModelName = process.env.THINY_MODEL ?? process.env.AGENT_MODEL ?? "openai:gpt-4o-mini";
|
|
1802
|
+
const personaName = process.env.THINY_PERSONA_NAME ?? "Thiny";
|
|
1803
|
+
const model = loadThinyConfig();
|
|
1804
|
+
const network = process.env.WALRUS_NETWORK === "mainnet" ? "mainnet" : "testnet";
|
|
1805
|
+
const walrus = walrusClient({
|
|
1806
|
+
network,
|
|
1807
|
+
publisher: process.env.WALRUS_PUBLISHER_URL,
|
|
1808
|
+
aggregator: process.env.WALRUS_AGGREGATOR_URL
|
|
1809
|
+
});
|
|
1810
|
+
const walrusAudit = envOn(process.env.WALRUS_AUDIT) ? walrusAuditLogger(logger, walrus, { sessionId: currentSessionId }) : null;
|
|
1811
|
+
const agentLogger = walrusAudit ?? logger;
|
|
1812
|
+
const userId = process.env.THINY_USER_ID ?? "default";
|
|
1813
|
+
const memwalEnabled = !!process.env.MEMWAL_DELEGATE_KEY && !!process.env.MEMWAL_ACCOUNT_ID;
|
|
1814
|
+
const memBackend = memwalEnabled ? "MemWal" : "Walrus";
|
|
1815
|
+
const memoryRefs = [];
|
|
1816
|
+
let pendingWrites = 0;
|
|
1817
|
+
let deliverRef = (ref) => memoryRefs.push(ref);
|
|
1818
|
+
const memoryPlugin = memwalEnabled ? memwalFactsPlugin({
|
|
1819
|
+
delegateKey: process.env.MEMWAL_DELEGATE_KEY,
|
|
1820
|
+
accountId: process.env.MEMWAL_ACCOUNT_ID,
|
|
1821
|
+
serverUrl: process.env.MEMWAL_SERVER_URL,
|
|
1822
|
+
namespace: process.env.MEMWAL_NAMESPACE ?? userId
|
|
1823
|
+
}) : walrusMemoryPlugin({
|
|
1824
|
+
client: walrus,
|
|
1825
|
+
pointers: filePointerStore(process.env.WALRUS_POINTERS ?? "thiny-pointers.json"),
|
|
1826
|
+
userId,
|
|
1827
|
+
onStoreStart: () => pendingWrites += 1,
|
|
1828
|
+
onStore: (ref) => {
|
|
1829
|
+
if (pendingWrites > 0) pendingWrites -= 1;
|
|
1830
|
+
deliverRef(ref);
|
|
1831
|
+
}
|
|
1832
|
+
});
|
|
1833
|
+
const configSkills = readThinyConfig().skills ?? [];
|
|
1834
|
+
const cliSkills = parseSkillArgs();
|
|
1835
|
+
const requestedSkillIds = [.../* @__PURE__ */ new Set([...configSkills, ...cliSkills])];
|
|
1836
|
+
const { plugins: skillPlugins, warnings: skillWarnings } = await loadSkills(
|
|
1837
|
+
requestedSkillIds,
|
|
1838
|
+
process.env
|
|
1839
|
+
);
|
|
1840
|
+
const persona = process.env.THINY_PERSONA_NAME ? { name: process.env.THINY_PERSONA_NAME, description: process.env.THINY_PERSONA_DESCRIPTION } : void 0;
|
|
1841
|
+
const budget = budgetMiddleware({ maxCalls: 50, logger });
|
|
1842
|
+
const agent = await createAgent({
|
|
1843
|
+
model,
|
|
1844
|
+
logger: agentLogger,
|
|
1845
|
+
persona,
|
|
1846
|
+
systemPrompt: "You are a helpful AI assistant. Use tools when they help you answer better. Be concise.\n\nMEMORY: You have persistent long-term memory across sessions, stored on Walrus. What you already know about the user is injected automatically at the start of each conversation under \u201C[User Memory \u2026]\u201D. When the user shares anything durable about themselves \u2014 their name, role, preferences, projects, or goals, even casually \u2014 immediately call remember_fact to save it. If asked what you remember, answer from the injected user memory (or call recall_memory). You DO remember across sessions \u2014 never say you lack memory or that each session starts fresh.\n\nFor multi-step work, call update_plan to track steps and delegate_task to hand focused sub-problems to a sub-agent.",
|
|
1847
|
+
tools: [echoTool],
|
|
1848
|
+
plugins: [
|
|
1849
|
+
{
|
|
1850
|
+
name: "observability",
|
|
1851
|
+
modelMiddleware: [modelAuditMiddleware(agentLogger), budget],
|
|
1852
|
+
toolMiddleware: [toolAuditMiddleware(agentLogger)]
|
|
1853
|
+
},
|
|
1854
|
+
agentsPlugin(),
|
|
1855
|
+
memoryPlugin,
|
|
1856
|
+
...skillPlugins
|
|
1857
|
+
]
|
|
1858
|
+
});
|
|
1859
|
+
clearScreen();
|
|
1860
|
+
renderHeader({
|
|
1861
|
+
model: activeModelName,
|
|
1862
|
+
session: currentSessionId,
|
|
1863
|
+
persona: personaName,
|
|
1864
|
+
version: "v0.1.0"
|
|
1865
|
+
});
|
|
1866
|
+
const registeredTools = agent.registry.all().map((t) => t.name).filter((name) => name !== "echo");
|
|
1867
|
+
const skillsByCategory = /* @__PURE__ */ new Map();
|
|
1868
|
+
if (requestedSkillIds.length > 0) {
|
|
1869
|
+
for (const id of requestedSkillIds) {
|
|
1870
|
+
const def = defaultRegistry.all().find((s) => s.id === id);
|
|
1871
|
+
if (!def) continue;
|
|
1872
|
+
const existing = skillsByCategory.get(def.category) ?? [];
|
|
1873
|
+
existing.push(def.id);
|
|
1874
|
+
skillsByCategory.set(def.category, existing);
|
|
1875
|
+
}
|
|
1876
|
+
} else {
|
|
1877
|
+
for (const [cat, defs] of defaultRegistry.byCategory()) {
|
|
1878
|
+
skillsByCategory.set(
|
|
1879
|
+
cat,
|
|
1880
|
+
defs.map((d) => d.id)
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
renderToolsAndSkills(registeredTools, skillsByCategory, {
|
|
1885
|
+
model: activeModelName,
|
|
1886
|
+
session: currentSessionId,
|
|
1887
|
+
persona: personaName
|
|
1888
|
+
});
|
|
1889
|
+
renderHints(logFile);
|
|
1890
|
+
for (const w of skillWarnings) renderWarning(w);
|
|
1891
|
+
renderInfo(
|
|
1892
|
+
`Memory: ${memwalEnabled ? "MemWal (semantic)" : "Walrus"} \xB7 cross-session, portable (user: ${userId})`
|
|
1893
|
+
);
|
|
1894
|
+
if (walrusAudit)
|
|
1895
|
+
renderInfo(`Walrus audit: ON (${network}) \u2014 each turn's action log is stored + verifiable`);
|
|
1896
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
1897
|
+
const spinner = new Spinner();
|
|
1898
|
+
const flushMemory = memoryPlugin.flush;
|
|
1899
|
+
const PROMPT = "\x1B[36mYou\x1B[0m \x1B[2m\u203A\x1B[0m ";
|
|
1900
|
+
let atPrompt = false;
|
|
1901
|
+
const printAbovePrompt = (fn) => {
|
|
1902
|
+
if (atPrompt) {
|
|
1903
|
+
cursorTo(stdout, 0);
|
|
1904
|
+
clearLine(stdout, 0);
|
|
1905
|
+
}
|
|
1906
|
+
fn();
|
|
1907
|
+
if (atPrompt) rl.prompt(true);
|
|
1908
|
+
};
|
|
1909
|
+
deliverRef = (ref) => {
|
|
1910
|
+
if (atPrompt) {
|
|
1911
|
+
printAbovePrompt(() => {
|
|
1912
|
+
renderStored("memory saved", explorerLinks(ref, network), memBackend);
|
|
1913
|
+
});
|
|
1914
|
+
} else memoryRefs.push(ref);
|
|
1915
|
+
};
|
|
1916
|
+
try {
|
|
1917
|
+
for (; ; ) {
|
|
1918
|
+
for (const ref of memoryRefs.splice(0))
|
|
1919
|
+
renderStored("memory saved", explorerLinks(ref, network), memBackend);
|
|
1920
|
+
if (pendingWrites > 0) renderSaving("memory", memBackend);
|
|
1921
|
+
atPrompt = true;
|
|
1922
|
+
const input = await rl.question(PROMPT);
|
|
1923
|
+
atPrompt = false;
|
|
1924
|
+
const trimmed = input.trim();
|
|
1925
|
+
if (!trimmed) continue;
|
|
1926
|
+
if (trimmed.startsWith("/")) {
|
|
1927
|
+
const parts = trimmed.slice(1).split(" ");
|
|
1928
|
+
const cmd = parts[0];
|
|
1929
|
+
switch (cmd) {
|
|
1930
|
+
case "new": {
|
|
1931
|
+
currentSessionId = `cli-${(/* @__PURE__ */ new Date()).getTime().toString()}`;
|
|
1932
|
+
renderInfo("New session started \u2014 long-term memory carries over");
|
|
1933
|
+
break;
|
|
1934
|
+
}
|
|
1935
|
+
case "tools":
|
|
1936
|
+
renderInfo(
|
|
1937
|
+
`
|
|
1938
|
+
Tools:
|
|
1939
|
+
${agent.registry.all().map((t) => ` \u2022 ${t.name} ${t.description.slice(0, 55)}`).join("\n")}
|
|
1940
|
+
`
|
|
1941
|
+
);
|
|
1942
|
+
break;
|
|
1943
|
+
case "skills":
|
|
1944
|
+
renderInfo("\nAvailable skills:");
|
|
1945
|
+
for (const [cat, defs] of defaultRegistry.byCategory()) {
|
|
1946
|
+
renderInfo(` [${cat}] ${defs.map((d) => d.id).join(", ")}`);
|
|
1947
|
+
}
|
|
1948
|
+
renderInfo("");
|
|
1949
|
+
break;
|
|
1950
|
+
case "session":
|
|
1951
|
+
renderInfo(`Session: ${currentSessionId}`);
|
|
1952
|
+
break;
|
|
1953
|
+
case "stats":
|
|
1954
|
+
renderInfo(
|
|
1955
|
+
`
|
|
1956
|
+
Session ${currentSessionId.slice(-8)} \xB7 ${String(session.turns)} turn${session.turns === 1 ? "" : "s"}
|
|
1957
|
+
tokens: \u2191${formatTokens(session.inputTokens)} \u2193${formatTokens(session.outputTokens)}
|
|
1958
|
+
tool calls: ${String(session.toolCalls)}
|
|
1959
|
+
`
|
|
1960
|
+
);
|
|
1961
|
+
break;
|
|
1962
|
+
case "verify": {
|
|
1963
|
+
const blobId = parts[1];
|
|
1964
|
+
if (!blobId) {
|
|
1965
|
+
renderWarning("usage: /verify <blobId>");
|
|
1966
|
+
break;
|
|
1967
|
+
}
|
|
1968
|
+
try {
|
|
1969
|
+
const trail = await verifyAuditTrail(walrus, blobId);
|
|
1970
|
+
renderInfo(
|
|
1971
|
+
`
|
|
1972
|
+
Audit trail ${blobId}
|
|
1973
|
+
session: ${trail.sessionId} \xB7 ${String(trail.count)} entries \xB7 ${trail.createdAt}`
|
|
1974
|
+
);
|
|
1975
|
+
for (const e of trail.entries) {
|
|
1976
|
+
const what = typeof e.kind === "string" ? e.kind : typeof e.event === "string" ? e.event : "";
|
|
1977
|
+
const tool = typeof e.tool === "string" ? ` (${e.tool})` : "";
|
|
1978
|
+
renderInfo(` \u2022 [${e.level}] ${what}${tool}`);
|
|
1979
|
+
}
|
|
1980
|
+
renderInfo(`
|
|
1981
|
+
source: ${walruscanBlobUrl(blobId, network)}
|
|
1982
|
+
`);
|
|
1983
|
+
} catch (err) {
|
|
1984
|
+
renderError(err instanceof Error ? err.message : String(err));
|
|
1985
|
+
}
|
|
1986
|
+
break;
|
|
1987
|
+
}
|
|
1988
|
+
case "clear":
|
|
1989
|
+
clearScreen();
|
|
1990
|
+
renderHeader({ model: activeModelName, session: currentSessionId, persona: personaName });
|
|
1991
|
+
renderToolsAndSkills(registeredTools, skillsByCategory, {
|
|
1992
|
+
model: activeModelName,
|
|
1993
|
+
session: currentSessionId,
|
|
1994
|
+
persona: personaName
|
|
1995
|
+
});
|
|
1996
|
+
renderHints(logFile);
|
|
1997
|
+
break;
|
|
1998
|
+
case "help":
|
|
1999
|
+
renderInfo(
|
|
2000
|
+
"\n/new \xB7 /tools \xB7 /skills \xB7 /stats \xB7 /session \xB7 /verify <blobId> \xB7 /clear \xB7 /help\n"
|
|
2001
|
+
);
|
|
2002
|
+
break;
|
|
2003
|
+
default:
|
|
2004
|
+
renderWarning(`Unknown command: /${cmd ?? ""} \u2014 try /help`);
|
|
2005
|
+
}
|
|
2006
|
+
continue;
|
|
2007
|
+
}
|
|
2008
|
+
renderAgentLabel(personaName);
|
|
2009
|
+
spinner.start("thinking\u2026");
|
|
2010
|
+
budget.reset();
|
|
2011
|
+
resetTurn(turn);
|
|
2012
|
+
const startedAt = Date.now();
|
|
2013
|
+
try {
|
|
2014
|
+
let firstToken = true;
|
|
2015
|
+
const stream = createMarkdownWriter((s) => stdout.write(s));
|
|
2016
|
+
const toolHandler = (payload) => {
|
|
2017
|
+
const { call } = payload;
|
|
2018
|
+
spinner.stop();
|
|
2019
|
+
stdout.write(` \x1B[33m\u2699\x1B[0m \x1B[2m${call.name}\x1B[0m
|
|
2020
|
+
`);
|
|
2021
|
+
spinner.start("running\u2026");
|
|
2022
|
+
};
|
|
2023
|
+
agent.events.on("beforeToolCall", toolHandler);
|
|
2024
|
+
const reply = await agent.run(trimmed, {
|
|
2025
|
+
sessionId: currentSessionId,
|
|
2026
|
+
onToken: (delta) => {
|
|
2027
|
+
spinner.stop();
|
|
2028
|
+
firstToken = false;
|
|
2029
|
+
stream.push(delta);
|
|
2030
|
+
}
|
|
2031
|
+
});
|
|
2032
|
+
agent.events.off("beforeToolCall", toolHandler);
|
|
2033
|
+
spinner.stop();
|
|
2034
|
+
if (firstToken) {
|
|
2035
|
+
stream.push(reply.length > 0 ? reply : "\x1B[2m(model returned empty response)\x1B[0m");
|
|
2036
|
+
}
|
|
2037
|
+
stream.end();
|
|
2038
|
+
renderAgentDone();
|
|
2039
|
+
const durMs = Date.now() - startedAt;
|
|
2040
|
+
session.turns += 1;
|
|
2041
|
+
session.inputTokens += turn.inputTokens;
|
|
2042
|
+
session.outputTokens += turn.outputTokens;
|
|
2043
|
+
session.toolCalls += turn.toolCalls;
|
|
2044
|
+
renderStatus([
|
|
2045
|
+
activeModelName,
|
|
2046
|
+
`${(durMs / 1e3).toFixed(1)}s`,
|
|
2047
|
+
`\u2191${formatTokens(turn.inputTokens)} \u2193${formatTokens(turn.outputTokens)}`,
|
|
2048
|
+
turn.toolCalls > 0 ? `${String(turn.toolCalls)} tool${turn.toolCalls === 1 ? "" : "s"}` : ""
|
|
2049
|
+
]);
|
|
2050
|
+
for (const ref of memoryRefs.splice(0))
|
|
2051
|
+
renderStored("memory saved", explorerLinks(ref, network), memBackend);
|
|
2052
|
+
if (walrusAudit && walrusAudit.entries().length > 0) {
|
|
2053
|
+
pendingWrites += 1;
|
|
2054
|
+
const flushP = walrusAudit.flush(currentSessionId);
|
|
2055
|
+
walrusAudit.reset();
|
|
2056
|
+
void flushP.then((ref) => {
|
|
2057
|
+
if (pendingWrites > 0) pendingWrites -= 1;
|
|
2058
|
+
if (ref) deliverRef(ref);
|
|
2059
|
+
}).catch((err) => {
|
|
2060
|
+
if (pendingWrites > 0) pendingWrites -= 1;
|
|
2061
|
+
const m = `Walrus audit flush failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
2062
|
+
if (atPrompt) {
|
|
2063
|
+
printAbovePrompt(() => {
|
|
2064
|
+
renderWarning(m);
|
|
2065
|
+
});
|
|
2066
|
+
} else renderWarning(m);
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
} catch (err) {
|
|
2070
|
+
spinner.stop();
|
|
2071
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2072
|
+
const looksLikeModelError = /\b(model|api|channel|base ?url|unauthorized|not found|invalid|401|404)\b/i.test(msg);
|
|
2073
|
+
renderError(
|
|
2074
|
+
looksLikeModelError ? `${msg}
|
|
2075
|
+
\u21B3 Check your model, base URL, and API key (run \`thiny init\`, or edit ~/.thiny/config.json / .env).` : msg
|
|
2076
|
+
);
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
} finally {
|
|
2080
|
+
if (flushMemory) await flushMemory().catch(() => void 0);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
// src/onboarding.ts
|
|
2085
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync, mkdirSync, chmodSync } from "fs";
|
|
2086
|
+
import { homedir } from "os";
|
|
2087
|
+
import { join, dirname as dirname2 } from "path";
|
|
2088
|
+
import { fileURLToPath } from "url";
|
|
2089
|
+
import * as p from "@clack/prompts";
|
|
2090
|
+
var THINY_DIR = join(homedir(), ".thiny");
|
|
2091
|
+
var CONFIG = join(THINY_DIR, "config.json");
|
|
2092
|
+
function version() {
|
|
2093
|
+
try {
|
|
2094
|
+
const pkg = join(dirname2(fileURLToPath(import.meta.url)), "../package.json");
|
|
2095
|
+
return JSON.parse(readFileSync2(pkg, "utf8")).version ?? "0.0.0";
|
|
2096
|
+
} catch {
|
|
2097
|
+
return "0.0.0";
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
function loadConfig() {
|
|
2101
|
+
return existsSync2(CONFIG) ? JSON.parse(readFileSync2(CONFIG, "utf8")) : null;
|
|
2102
|
+
}
|
|
2103
|
+
function saveConfig(cfg) {
|
|
2104
|
+
mkdirSync(THINY_DIR, { recursive: true });
|
|
2105
|
+
chmodSync(THINY_DIR, 448);
|
|
2106
|
+
writeFileSync(CONFIG, JSON.stringify(cfg, null, 2));
|
|
2107
|
+
chmodSync(CONFIG, 384);
|
|
2108
|
+
}
|
|
2109
|
+
function applyConfig(cfg) {
|
|
2110
|
+
if (!cfg) return;
|
|
2111
|
+
const set = (k, v) => {
|
|
2112
|
+
if (v && !process.env[k]) process.env[k] = v;
|
|
2113
|
+
};
|
|
2114
|
+
set("THINY_MODEL", cfg.model);
|
|
2115
|
+
if (cfg.apiKey) {
|
|
2116
|
+
set(cfg.model?.startsWith("anthropic") ? "THINY_ANTHROPIC_API_KEY" : "THINY_OPENAI_API_KEY", cfg.apiKey);
|
|
2117
|
+
}
|
|
2118
|
+
set("THINY_OPENAI_BASE_URL", cfg.baseUrl);
|
|
2119
|
+
set("THINY_PERSONA_NAME", cfg.agentName);
|
|
2120
|
+
set("THINY_USER_ID", cfg.userId);
|
|
2121
|
+
if (cfg.sui?.network) {
|
|
2122
|
+
set("SUI_NETWORK", cfg.sui.network);
|
|
2123
|
+
if (cfg.sui.network === "mainnet") set("SUI_ALLOW_MAINNET", "1");
|
|
2124
|
+
}
|
|
2125
|
+
const sk = cfg.sui?.wallet.secretKey;
|
|
2126
|
+
if (sk) {
|
|
2127
|
+
set("SUI_SECRET_KEY", sk);
|
|
2128
|
+
set("THINY_SUI_SECRET_KEY", sk);
|
|
2129
|
+
}
|
|
2130
|
+
set("MCP_URL", cfg.sui?.rillMcpUrl);
|
|
2131
|
+
}
|
|
2132
|
+
function bail(v) {
|
|
2133
|
+
if (p.isCancel(v)) {
|
|
2134
|
+
p.cancel("Cancelled.");
|
|
2135
|
+
process.exit(0);
|
|
2136
|
+
}
|
|
2137
|
+
return v;
|
|
2138
|
+
}
|
|
2139
|
+
var MODELS = [
|
|
2140
|
+
{ value: "oai-mini", label: "OpenAI \xB7 gpt-4o-mini", hint: "fast, cheap", model: "openai:gpt-4o-mini", needsKey: true },
|
|
2141
|
+
{ value: "oai-4o", label: "OpenAI \xB7 gpt-4o", model: "openai:gpt-4o", needsKey: true },
|
|
2142
|
+
{ value: "claude-haiku", label: "Anthropic \xB7 claude-haiku-4-5", model: "anthropic:claude-haiku-4-5-20251001", needsKey: true },
|
|
2143
|
+
{ value: "claude-sonnet", label: "Anthropic \xB7 claude-sonnet-4-6", model: "anthropic:claude-sonnet-4-6", needsKey: true },
|
|
2144
|
+
{ value: "ollama", label: "Ollama", hint: "local, no key", model: "llama3", baseUrl: "http://localhost:11434/v1", apiKey: "ollama" },
|
|
2145
|
+
{ value: "custom", label: "Custom", hint: "any OpenAI-compatible endpoint", custom: true }
|
|
2146
|
+
];
|
|
2147
|
+
async function baseSetup() {
|
|
2148
|
+
p.intro(`Thiny ${version()} \u2014 setup`);
|
|
2149
|
+
const agentName = bail(
|
|
2150
|
+
await p.text({ message: "Agent name", placeholder: "ThinyAI", defaultValue: "ThinyAI" })
|
|
2151
|
+
);
|
|
2152
|
+
const choice = bail(
|
|
2153
|
+
await p.select({ message: "Pick a model", options: MODELS.map(({ value, label, hint }) => ({ value, label, hint })) })
|
|
2154
|
+
);
|
|
2155
|
+
const pick = MODELS.find((m) => m.value === choice) ?? MODELS[0];
|
|
2156
|
+
const cfg = { agentName, userId: "default" };
|
|
2157
|
+
if (pick.custom) {
|
|
2158
|
+
cfg.model = bail(
|
|
2159
|
+
await p.text({ message: "Model id", placeholder: "e.g. MiniMax-M3", validate: (v) => v ? void 0 : "Required" })
|
|
2160
|
+
);
|
|
2161
|
+
cfg.baseUrl = bail(
|
|
2162
|
+
await p.text({
|
|
2163
|
+
message: "Base URL (OpenAI-compatible)",
|
|
2164
|
+
placeholder: "https://api.example.com/v1",
|
|
2165
|
+
validate: (v) => /^https?:\/\//.test(v) ? void 0 : "Must start with http(s)://"
|
|
2166
|
+
})
|
|
2167
|
+
);
|
|
2168
|
+
cfg.apiKey = bail(await p.password({ message: "API key" }));
|
|
2169
|
+
} else {
|
|
2170
|
+
cfg.model = pick.model;
|
|
2171
|
+
if (pick.baseUrl) cfg.baseUrl = pick.baseUrl;
|
|
2172
|
+
cfg.apiKey = pick.apiKey ?? (pick.needsKey ? bail(await p.password({ message: "API key" })) : void 0);
|
|
2173
|
+
}
|
|
2174
|
+
saveConfig(cfg);
|
|
2175
|
+
p.outro(`Saved ${CONFIG} \u2014 run \`thiny\` to start, or \`thiny sui init\` for Sui.`);
|
|
2176
|
+
return cfg;
|
|
2177
|
+
}
|
|
2178
|
+
async function suiInit() {
|
|
2179
|
+
let cfg = loadConfig();
|
|
2180
|
+
cfg ??= await baseSetup();
|
|
2181
|
+
p.intro("Thiny \u2014 Sui setup");
|
|
2182
|
+
const network = bail(
|
|
2183
|
+
await p.select({
|
|
2184
|
+
message: "Sui network (you can change this later)",
|
|
2185
|
+
options: [
|
|
2186
|
+
{ value: "testnet", label: "Testnet", hint: "recommended for testing" },
|
|
2187
|
+
{ value: "mainnet", label: "Mainnet", hint: "real funds" }
|
|
2188
|
+
]
|
|
2189
|
+
})
|
|
2190
|
+
);
|
|
2191
|
+
const choice = bail(
|
|
2192
|
+
await p.select({
|
|
2193
|
+
message: "Wallet",
|
|
2194
|
+
options: [
|
|
2195
|
+
{ value: "paste", label: "Paste an existing private key", hint: "suiprivkey\u2026" },
|
|
2196
|
+
{ value: "generate", label: "Generate a new key pair locally" },
|
|
2197
|
+
{ value: "rill", label: "Agent wallet with on-chain capabilities", hint: "Rill" }
|
|
2198
|
+
]
|
|
2199
|
+
})
|
|
2200
|
+
);
|
|
2201
|
+
const { Ed25519Keypair } = await import("@mysten/sui/keypairs/ed25519");
|
|
2202
|
+
let wallet;
|
|
2203
|
+
let address;
|
|
2204
|
+
if (choice === "generate" || choice === "rill") {
|
|
2205
|
+
const kp = Ed25519Keypair.generate();
|
|
2206
|
+
wallet = { type: "generated", secretKey: kp.getSecretKey() };
|
|
2207
|
+
address = kp.getPublicKey().toSuiAddress();
|
|
2208
|
+
} else {
|
|
2209
|
+
const sk = bail(
|
|
2210
|
+
await p.password({
|
|
2211
|
+
message: "Private key (suiprivkey\u2026)",
|
|
2212
|
+
validate: (v) => v.startsWith("suiprivkey") ? void 0 : "Expected a suiprivkey\u2026 string"
|
|
2213
|
+
})
|
|
2214
|
+
);
|
|
2215
|
+
wallet = { type: "imported", secretKey: sk };
|
|
2216
|
+
address = Ed25519Keypair.fromSecretKey(sk).getPublicKey().toSuiAddress();
|
|
2217
|
+
}
|
|
2218
|
+
cfg.sui = { network, wallet, address };
|
|
2219
|
+
if (choice === "rill") {
|
|
2220
|
+
const url = bail(
|
|
2221
|
+
await p.text({ message: "Rill MCP URL", placeholder: "leave blank to add later", defaultValue: "" })
|
|
2222
|
+
);
|
|
2223
|
+
if (url) cfg.sui.rillMcpUrl = url;
|
|
2224
|
+
}
|
|
2225
|
+
saveConfig(cfg);
|
|
2226
|
+
const faucet = network === "testnet" ? "\nFaucet: https://faucet.sui.io (or `sui client faucet`)" : "";
|
|
2227
|
+
p.note(`${address}${faucet}`, `\u26A0 Fund this address (${network}) before sending transactions`);
|
|
2228
|
+
p.outro(`Sui configured (${network}).`);
|
|
2229
|
+
}
|
|
2230
|
+
async function ensureSetup() {
|
|
2231
|
+
if (loadConfig() || process.env.THINY_MODEL) return;
|
|
2232
|
+
await baseSetup();
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
// src/bin.ts
|
|
2236
|
+
function help() {
|
|
2237
|
+
console.log(`thiny ${version()}
|
|
2238
|
+
|
|
2239
|
+
Usage:
|
|
2240
|
+
thiny Start the interactive CLI agent (runs setup on first use)
|
|
2241
|
+
thiny init Re-run setup (model, agent name, key)
|
|
2242
|
+
thiny sui init Add Sui capabilities (network + wallet)
|
|
2243
|
+
thiny --version Print version
|
|
2244
|
+
thiny help Show this help
|
|
2245
|
+
|
|
2246
|
+
Config: ~/.thiny/config.json (no .env needed)`);
|
|
2247
|
+
}
|
|
2248
|
+
async function run() {
|
|
2249
|
+
const [sub, sub2] = process.argv.slice(2);
|
|
2250
|
+
switch (sub) {
|
|
2251
|
+
case "init":
|
|
2252
|
+
await baseSetup();
|
|
2253
|
+
return;
|
|
2254
|
+
case "sui":
|
|
2255
|
+
if (sub2 === "init") await suiInit();
|
|
2256
|
+
else console.log("Usage: thiny sui init");
|
|
2257
|
+
return;
|
|
2258
|
+
case "--version":
|
|
2259
|
+
case "-v":
|
|
2260
|
+
console.log(version());
|
|
2261
|
+
return;
|
|
2262
|
+
case "help":
|
|
2263
|
+
case "--help":
|
|
2264
|
+
case "-h":
|
|
2265
|
+
help();
|
|
2266
|
+
return;
|
|
2267
|
+
default:
|
|
2268
|
+
await ensureSetup();
|
|
2269
|
+
applyConfig(loadConfig());
|
|
2270
|
+
await runCli();
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
run().catch((err) => {
|
|
2274
|
+
process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}
|
|
2275
|
+
`);
|
|
2276
|
+
process.exit(1);
|
|
2277
|
+
});
|