@genkaku/protocol 0.1.0 → 0.1.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AACpC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,MAAM,CAAA;AAa/B,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACpD,OAAO,EAAgB,MAAM,EAAc,MAAM,YAAY,CAAA;AAI7D,uEAAuE;AACvE,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,UAAU,CAAC,OAAO,MAAM,CAAC,CAAA;KAAE,CAAC,CAAA;IACjE,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,cAAc,CAAA;IACnB,WAAW,EAAE,WAAW,CAAA;IACxB,OAAO,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;IAC9B,OAAO,EAAE,MAAM,GAAG,CAAA;IAClB,yEAAyE;IACzE,MAAM,EAAE,OAAO,CAAA;IACf,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC3B,wFAAwF;IACxF,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IAC3F,wDAAwD;IACxD,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjD;4DACwD;IACxD,MAAM,CAAC,EAAE,CACP,OAAO,EAAE,MAAM,EAAE,KACd,GAAG,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAA;CAClH,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B,CAAA;AA8ID,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAmblF"}
1
+ {"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AACpC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,MAAM,CAAA;AAc/B,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACpD,OAAO,EAAgB,MAAM,EAAc,MAAM,YAAY,CAAA;AAI7D,uEAAuE;AACvE,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,UAAU,CAAC,OAAO,MAAM,CAAC,CAAA;KAAE,CAAC,CAAA;IACjE,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,cAAc,CAAA;IACnB,WAAW,EAAE,WAAW,CAAA;IACxB,OAAO,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;IAC9B,OAAO,EAAE,MAAM,GAAG,CAAA;IAClB,yEAAyE;IACzE,MAAM,EAAE,OAAO,CAAA;IACf,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC3B,wFAAwF;IACxF,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IAC3F,wDAAwD;IACxD,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjD;4DACwD;IACxD,MAAM,CAAC,EAAE,CACP,OAAO,EAAE,MAAM,EAAE,KACd,GAAG,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAA;CAClH,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B,CAAA;AAuJD,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAmflF"}
package/dist/bridge.js CHANGED
@@ -10,6 +10,8 @@ import { dedupeOffers, toWire } from "./offer.js";
10
10
  import { bindingOf, peerIdOf } from "./registry.js";
11
11
  import { runUserSession } from "./userSession.js";
12
12
  const OFFERS_TTL_MS = 10_000;
13
+ /** Ceiling for one request's completion length; the hourly quote bounds cost. */
14
+ const MAX_COMPLETION_TOKENS = 4096;
13
15
  const SSE_TAIL = String.fromCharCode(10, 10);
14
16
  const SSE_DONE = 'data: ' + String.fromCharCode(91, 68, 79, 78, 69, 93) + SSE_TAIL;
15
17
  function textOf(content) {
@@ -202,14 +204,25 @@ export async function startOpenAiBridge(opts) {
202
204
  }
203
205
  const models = async (res) => {
204
206
  const rows = await board();
205
- const seen = new Set();
206
- const data = [];
207
+ const seen = new Map();
207
208
  for (const { offer } of rows) {
208
209
  if (seen.has(offer.model))
209
210
  continue;
210
- seen.add(offer.model);
211
- data.push({ id: offer.model, object: 'model', owned_by: 'genkaku' });
211
+ // A v0 row carries no capability facts, so it is chat-only by definition.
212
+ seen.set(offer.model, {
213
+ tier: offer.tier ?? 'chat',
214
+ capabilities: offer.capabilities ?? [],
215
+ ...(offer.parameterSize === undefined ? {} : { parameterSize: offer.parameterSize }),
216
+ });
212
217
  }
218
+ const data = [...seen.entries()].map(([id, facts]) => ({
219
+ id,
220
+ object: 'model',
221
+ created: 0,
222
+ owned_by: 'genkaku',
223
+ // Non-standard, ignored by OpenAI clients and read by our selector.
224
+ genkaku: facts,
225
+ }));
213
226
  json(res, 200, { object: 'list', data });
214
227
  };
215
228
  const status = async (res) => {
@@ -254,6 +267,21 @@ export async function startOpenAiBridge(opts) {
254
267
  .join('\n');
255
268
  if (prompt === '')
256
269
  return failure(res, 400, 'no user message');
270
+ // A request that carries tool schemas, or whose history already contains
271
+ // tool calls/results, needs the chat revision: the single-prompt form would
272
+ // silently drop the very thing an agent turn depends on.
273
+ const needsChat = Array.isArray(body.tools) && body.tools.length > 0
274
+ || messages.some(m => m.role === 'tool' || Array.isArray(m.tool_calls));
275
+ const conversation = needsChat
276
+ ? messages
277
+ .filter(m => m.role === 'system' || m.role === 'user' || m.role === 'assistant' || m.role === 'tool')
278
+ .map(m => ({
279
+ role: m.role,
280
+ content: textOf(m.content),
281
+ ...(Array.isArray(m.tool_calls) ? { tool_calls: m.tool_calls } : {}),
282
+ ...(typeof m.tool_call_id === 'string' ? { tool_call_id: m.tool_call_id } : {}),
283
+ }))
284
+ : undefined;
257
285
  const rows = await board();
258
286
  const picked = rows.find(row => row.offer.model === model) ?? (rows.length === 1 ? rows[0] : undefined);
259
287
  if (picked == null)
@@ -294,9 +322,12 @@ export async function startOpenAiBridge(opts) {
294
322
  const created = Math.floor(Date.now() / 1000);
295
323
  const id = `chatcmpl-${jobId}`;
296
324
  const sse = body.stream !== false;
325
+ // Cost is bounded by the hourly quote, not by starving the completion: an
326
+ // agent needs room for a tool call and a short answer. 4096 is the ceiling
327
+ // any single request may ask for.
297
328
  const maxTokens = Math.min(typeof body.max_tokens === 'number' ? body.max_tokens
298
329
  : typeof body.max_completion_tokens === 'number' ? body.max_completion_tokens
299
- : 256, 96);
330
+ : 1024, MAX_COMPLETION_TOKENS);
300
331
  let escrowId = '0';
301
332
  let walletC = null;
302
333
  if (opts.settle && picked.offer.workerWallet?.startsWith('0x')) {
@@ -335,6 +366,8 @@ export async function startOpenAiBridge(opts) {
335
366
  res.write(`data: ${JSON.stringify(chunk)}\n\n`);
336
367
  };
337
368
  let receiptHash = null;
369
+ let sawToolCall = false;
370
+ const toolCalls = [];
338
371
  let content = '';
339
372
  const abort = () => { if (!res.writableEnded)
340
373
  void transport.close(); };
@@ -347,6 +380,8 @@ export async function startOpenAiBridge(opts) {
347
380
  prompt,
348
381
  system: system === '' ? undefined : system,
349
382
  maxTokens,
383
+ ...(conversation === undefined ? {} : { messages: conversation }),
384
+ ...(body.tools === undefined ? {} : { tools: body.tools }),
350
385
  transport,
351
386
  modeManager: opts.modeManager,
352
387
  escrowId,
@@ -357,6 +392,24 @@ export async function startOpenAiBridge(opts) {
357
392
  content += event.delta;
358
393
  writeChunk({ content: event.delta }, null);
359
394
  }
395
+ else if (event.type === 'tool-delta') {
396
+ sawToolCall = true;
397
+ const existing = toolCalls.find(call => call.index === event.index);
398
+ if (existing == null) {
399
+ toolCalls.push({ index: event.index, id: event.id, name: event.name, arguments: event.argumentsDelta });
400
+ }
401
+ else {
402
+ existing.arguments += event.argumentsDelta;
403
+ }
404
+ writeChunk({
405
+ tool_calls: [{
406
+ index: event.index,
407
+ id: event.id,
408
+ type: 'function',
409
+ function: { name: event.name, arguments: event.argumentsDelta },
410
+ }],
411
+ }, null);
412
+ }
360
413
  else if (event.type === 'receipt-sent') {
361
414
  receiptHash = event.receiptHash;
362
415
  }
@@ -377,7 +430,7 @@ export async function startOpenAiBridge(opts) {
377
430
  total_tokens: Math.ceil(prompt.length / 4) + result.tokens,
378
431
  };
379
432
  if (sse) {
380
- writeChunk({}, 'stop', usage); // finish
433
+ writeChunk({}, sawToolCall ? 'tool_calls' : 'stop', usage); // finish
381
434
  res.write(SSE_DONE);
382
435
  res.end();
383
436
  }
@@ -387,7 +440,21 @@ export async function startOpenAiBridge(opts) {
387
440
  object: 'chat.completion',
388
441
  created,
389
442
  model: picked.offer.model,
390
- choices: [{ index: 0, message: { role: 'assistant', content }, finish_reason: 'stop' }],
443
+ choices: [{
444
+ index: 0,
445
+ message: {
446
+ role: 'assistant',
447
+ content,
448
+ ...(toolCalls.length === 0 ? {} : {
449
+ tool_calls: toolCalls.map(call => ({
450
+ id: call.id,
451
+ type: 'function',
452
+ function: { name: call.name, arguments: call.arguments },
453
+ })),
454
+ }),
455
+ },
456
+ finish_reason: sawToolCall ? 'tool_calls' : 'stop',
457
+ }],
391
458
  usage,
392
459
  });
393
460
  }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * What a shared model can actually do.
3
+ *
4
+ * Ollama reports a `capabilities` list per model (`completion`, `tools`,
5
+ * `thinking`, `vision`, …). A model that lists `tools` accepts the field, which
6
+ * is not the same as calling tools well: a 3B model usually emits malformed
7
+ * calls. So the tier combines the capability with the parameter count, and the
8
+ * UI labels only the bottom tier. Nothing here blocks a model from being
9
+ * offered — the marketplace marks it instead.
10
+ */
11
+ export type ModelTier = 'agent' | 'light' | 'chat';
12
+ export type ModelProfile = {
13
+ model: string;
14
+ /** raw capability list from Ollama, e.g. ['completion', 'tools'] */
15
+ capabilities: string[];
16
+ /** raw `details.parameter_size`, e.g. '9.0B' */
17
+ parameterSize?: string;
18
+ /** billions of parameters, parsed from `parameterSize` when possible */
19
+ parametersB?: number;
20
+ tier: ModelTier;
21
+ };
22
+ /** Size a model needs before its tool calls are worth advertising to an agent. */
23
+ export declare const AGENT_MIN_BILLIONS = 9;
24
+ /** Below this a model with the `tools` capability is only a light agent. */
25
+ export declare const LIGHT_MIN_BILLIONS = 3;
26
+ /**
27
+ * Parse Ollama's `details.parameter_size` ('1.2B', '9b', '70B', '135M').
28
+ * @param size - the raw string from `/api/tags`.
29
+ * @returns billions of parameters, or undefined when the string is not a size.
30
+ */
31
+ export declare function parseParameterSize(size: string | undefined): number | undefined;
32
+ /**
33
+ * Classify a model for the marketplace.
34
+ *
35
+ * @param capabilities - Ollama's capability list.
36
+ * @param parametersB - billions of parameters, when known.
37
+ */
38
+ export declare function tierOf(capabilities: readonly string[], parametersB?: number): ModelTier;
39
+ /** Shape returned by the probe, independent of HTTP plumbing. */
40
+ export type OllamaModelFacts = {
41
+ capabilities?: string[];
42
+ parameterSize?: string;
43
+ };
44
+ /**
45
+ * Fold `/api/show` capabilities and `/api/tags` details into one profile.
46
+ * @param model - model name as Ollama knows it.
47
+ * @param facts - whatever the two endpoints reported.
48
+ */
49
+ export declare function profileOf(model: string, facts: OllamaModelFacts): ModelProfile;
50
+ //# sourceMappingURL=capabilities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAA;AAElD,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,oEAAoE;IACpE,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,gDAAgD;IAChD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,EAAE,SAAS,CAAA;CAChB,CAAA;AAED,kFAAkF;AAClF,eAAO,MAAM,kBAAkB,IAAI,CAAA;AACnC,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAO/E;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAQvF;AAED,iEAAiE;AACjE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,CAAA;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,YAAY,CAU9E"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * What a shared model can actually do.
3
+ *
4
+ * Ollama reports a `capabilities` list per model (`completion`, `tools`,
5
+ * `thinking`, `vision`, …). A model that lists `tools` accepts the field, which
6
+ * is not the same as calling tools well: a 3B model usually emits malformed
7
+ * calls. So the tier combines the capability with the parameter count, and the
8
+ * UI labels only the bottom tier. Nothing here blocks a model from being
9
+ * offered — the marketplace marks it instead.
10
+ */
11
+ /** Size a model needs before its tool calls are worth advertising to an agent. */
12
+ export const AGENT_MIN_BILLIONS = 9;
13
+ /** Below this a model with the `tools` capability is only a light agent. */
14
+ export const LIGHT_MIN_BILLIONS = 3;
15
+ /**
16
+ * Parse Ollama's `details.parameter_size` ('1.2B', '9b', '70B', '135M').
17
+ * @param size - the raw string from `/api/tags`.
18
+ * @returns billions of parameters, or undefined when the string is not a size.
19
+ */
20
+ export function parseParameterSize(size) {
21
+ if (size == null)
22
+ return undefined;
23
+ const match = /^\s*([\d.]+)\s*([bm])/i.exec(size);
24
+ if (match == null)
25
+ return undefined;
26
+ const value = Number.parseFloat(match[1] ?? '');
27
+ if (!Number.isFinite(value))
28
+ return undefined;
29
+ return match[2]?.toLowerCase() === 'm' ? value / 1000 : value;
30
+ }
31
+ /**
32
+ * Classify a model for the marketplace.
33
+ *
34
+ * @param capabilities - Ollama's capability list.
35
+ * @param parametersB - billions of parameters, when known.
36
+ */
37
+ export function tierOf(capabilities, parametersB) {
38
+ if (!capabilities.includes('tools'))
39
+ return 'chat';
40
+ // Unknown size with the tools capability: treat as a light agent rather than
41
+ // claiming it can drive an agent loop.
42
+ if (parametersB == null)
43
+ return 'light';
44
+ if (parametersB >= AGENT_MIN_BILLIONS)
45
+ return 'agent';
46
+ if (parametersB >= LIGHT_MIN_BILLIONS)
47
+ return 'light';
48
+ return 'chat';
49
+ }
50
+ /**
51
+ * Fold `/api/show` capabilities and `/api/tags` details into one profile.
52
+ * @param model - model name as Ollama knows it.
53
+ * @param facts - whatever the two endpoints reported.
54
+ */
55
+ export function profileOf(model, facts) {
56
+ const capabilities = facts.capabilities ?? [];
57
+ const parametersB = parseParameterSize(facts.parameterSize);
58
+ return {
59
+ model,
60
+ capabilities,
61
+ ...(facts.parameterSize === undefined ? {} : { parameterSize: facts.parameterSize }),
62
+ ...(parametersB === undefined ? {} : { parametersB }),
63
+ tier: tierOf(capabilities, parametersB),
64
+ };
65
+ }
66
+ //# sourceMappingURL=capabilities.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=capabilities.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities.test.d.ts","sourceRoot":"","sources":["../src/capabilities.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,39 @@
1
+ import assert from 'node:assert/strict';
2
+ import { test } from 'node:test';
3
+ import { parseParameterSize, profileOf, tierOf } from "./capabilities.js";
4
+ test('parseParameterSize reads the forms Ollama reports', () => {
5
+ assert.equal(parseParameterSize('1.2B'), 1.2);
6
+ assert.equal(parseParameterSize('9b'), 9);
7
+ assert.equal(parseParameterSize('70B'), 70);
8
+ assert.equal(parseParameterSize('135M'), 0.135);
9
+ assert.equal(parseParameterSize(undefined), undefined);
10
+ assert.equal(parseParameterSize('unknown'), undefined);
11
+ });
12
+ test('a model without the tools capability is chat-only, whatever its size', () => {
13
+ assert.equal(tierOf(['completion'], 70), 'chat');
14
+ assert.equal(tierOf([], 70), 'chat');
15
+ assert.equal(tierOf(['completion', 'vision'], 70), 'chat');
16
+ });
17
+ test('tools + size decides agent or light', () => {
18
+ assert.equal(tierOf(['completion', 'tools'], 9), 'agent');
19
+ assert.equal(tierOf(['completion', 'tools'], 27), 'agent');
20
+ assert.equal(tierOf(['completion', 'tools'], 3.8), 'light');
21
+ assert.equal(tierOf(['completion', 'tools'], 1.2), 'chat');
22
+ });
23
+ test('tools with an unknown size is light, never agent', () => {
24
+ assert.equal(tierOf(['completion', 'tools'], undefined), 'light');
25
+ });
26
+ test('profileOf folds both endpoints into one row', () => {
27
+ const profile = profileOf('qwen3.8:27b', { capabilities: ['completion', 'tools', 'thinking'], parameterSize: '27B' });
28
+ assert.deepEqual(profile, {
29
+ model: 'qwen3.8:27b',
30
+ capabilities: ['completion', 'tools', 'thinking'],
31
+ parameterSize: '27B',
32
+ parametersB: 27,
33
+ tier: 'agent',
34
+ });
35
+ });
36
+ test('a failed probe degrades to chat instead of claiming agent support', () => {
37
+ assert.equal(profileOf('mystery', {}).tier, 'chat');
38
+ });
39
+ //# sourceMappingURL=capabilities.test.js.map
package/dist/dial.d.ts CHANGED
@@ -1,10 +1,18 @@
1
1
  import type { Libp2p } from 'libp2p';
2
2
  import { type HelloProof } from './hello.ts';
3
3
  import { type Offer } from './offer.ts';
4
- import type { OpenEnvelopeV0 } from './workerSession.ts';
4
+ import type { OpenEnvelope } from './workerSession.ts';
5
+ export type WorkerToolCall = {
6
+ index: number;
7
+ id: string;
8
+ name: string;
9
+ argumentsDelta: string;
10
+ };
5
11
  export type WorkerTransport = {
6
- open(env: OpenEnvelopeV0): Promise<void>;
12
+ open(env: OpenEnvelope): Promise<void>;
7
13
  onToken(cb: (delta: string) => void): void;
14
+ /** Tool calls relayed by a worker serving `open/1`. */
15
+ onToolCall(cb: (call: WorkerToolCall) => void): void;
8
16
  onEnd(cb: () => void): void;
9
17
  onError(cb: (e: Error) => void): void;
10
18
  /** sig: EIP-191 signature over the packed receipt digest (P4.4 priced
@@ -40,4 +48,11 @@ export declare function requestSettleCosign(node: Libp2p, addr: string, args: {
40
48
  sig: `0x${string}` | null;
41
49
  reason?: string;
42
50
  }>;
51
+ /**
52
+ * Tell a seed which addresses this worker can be reached on.
53
+ *
54
+ * Without this the seed can only answer with itself, which is no help to a cold
55
+ * client. A seed that does not implement the protocol is ignored.
56
+ */
57
+ export declare function announceToSeed(node: Libp2p, seed: string, addrs: string[]): Promise<boolean>;
43
58
  //# sourceMappingURL=dial.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dial.d.ts","sourceRoot":"","sources":["../src/dial.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAEpC,OAAO,EAAyB,KAAK,UAAU,EAAE,MAAM,YAAY,CAAA;AAEnE,OAAO,EAAY,KAAK,KAAK,EAAE,MAAM,YAAY,CAAA;AAGjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAQxD,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACxC,OAAO,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAA;IAC1C,KAAK,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAAA;IAC3B,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAA;IACrC;6DACyD;IACzD,WAAW,CACT,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EACd,GAAG,CAAC,EAAE,MAAM,EACZ,SAAS,CAAC,EAAE,KAAK,MAAM,EAAE,EACzB,UAAU,CAAC,EAAE,KAAK,MAAM,EAAE,GACzB,OAAO,CAAC,IAAI,CAAC,CAAA;IAChB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB,CAAA;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAsIrF;AAED,+EAA+E;AAC/E;;gEAEgE;AAChE,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAqDjF;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAqB9E;AAuBD;+EAC+E;AAC/E,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAmBlF;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,KAAK,MAAM,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACnF,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,KAAK,MAAM,EAAE,GAAG,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAgCtE"}
1
+ {"version":3,"file":"dial.d.ts","sourceRoot":"","sources":["../src/dial.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAEpC,OAAO,EAAyB,KAAK,UAAU,EAAE,MAAM,YAAY,CAAA;AAEnE,OAAO,EAAY,KAAK,KAAK,EAAE,MAAM,YAAY,CAAA;AAGjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAQtD,MAAM,MAAM,cAAc,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,CAAA;AAEhG,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACtC,OAAO,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAA;IAC1C,uDAAuD;IACvD,UAAU,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,GAAG,IAAI,CAAA;IACpD,KAAK,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAAA;IAC3B,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAA;IACrC;6DACyD;IACzD,WAAW,CACT,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EACd,GAAG,CAAC,EAAE,MAAM,EACZ,SAAS,CAAC,EAAE,KAAK,MAAM,EAAE,EACzB,UAAU,CAAC,EAAE,KAAK,MAAM,EAAE,GACzB,OAAO,CAAC,IAAI,CAAC,CAAA;IAChB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB,CAAA;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAwJrF;AAED,+EAA+E;AAC/E;;gEAEgE;AAChE,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAqDjF;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAqB9E;AA4BD;+EAC+E;AAC/E,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAkClF;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,KAAK,MAAM,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACnF,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,KAAK,MAAM,EAAE,GAAG,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAgCtE;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAelG"}
package/dist/dial.js CHANGED
@@ -21,6 +21,7 @@ export async function dialWorker(node, addr) {
21
21
  let openSinkDone = Promise.resolve();
22
22
  let receiptSinkDone = Promise.resolve();
23
23
  const tokenCbs = new Set();
24
+ const toolCbs = new Set();
24
25
  const endCbs = new Set();
25
26
  const errCbs = new Set();
26
27
  /** Resolved once the worker admits the session (ok/0) or sends a token;
@@ -66,6 +67,18 @@ export async function dialWorker(node, addr) {
66
67
  cb(String(f.delta ?? ''));
67
68
  }
68
69
  }
70
+ else if (f.proto === 'tool/0') {
71
+ admitted?.();
72
+ const call = {
73
+ index: Number(f.index ?? 0),
74
+ id: String(f.id ?? ''),
75
+ name: String(f.name ?? ''),
76
+ argumentsDelta: String(f.argumentsDelta ?? ''),
77
+ };
78
+ for (const cb of toolCbs) {
79
+ cb(call);
80
+ }
81
+ }
69
82
  }
70
83
  if (dead) {
71
84
  break;
@@ -110,6 +123,9 @@ export async function dialWorker(node, addr) {
110
123
  refused = null;
111
124
  }
112
125
  },
126
+ onToolCall(cb) {
127
+ toolCbs.add(cb);
128
+ },
113
129
  onToken(cb) { tokenCbs.add(cb); },
114
130
  onEnd(cb) { endCbs.add(cb); },
115
131
  onError(cb) { errCbs.add(cb); },
@@ -205,17 +221,39 @@ async function fetchOfferBoard(node, addr) {
205
221
  for await (const chunk of s.source) {
206
222
  reader.push(chunk);
207
223
  const f = reader.next();
208
- if (f?.proto === 'offers/0' && Array.isArray(f.board)) {
224
+ if ((f?.proto === 'offers/0' || f?.proto === 'offers/1') && Array.isArray(f.board)) {
209
225
  board = f.board;
210
226
  break;
211
227
  }
212
228
  }
213
229
  await s.close().catch(() => { });
214
- return board.filter(w => w.proto === 'offers/0').map(w => fromWire(w));
230
+ return board
231
+ .filter(w => {
232
+ const proto = w.proto;
233
+ return proto === 'offers/0' || proto === 'offers/1';
234
+ })
235
+ .map(w => fromWire(w));
215
236
  }
216
237
  /** P4.5 seed discovery: ask a bootstrap seed which peers it currently sees.
217
238
  * The seed is a pure matchmaker — one frame in, one frame out, no storage. */
218
239
  export async function fetchSeedPeers(node, addr) {
240
+ // A seed addressed by URL is read over HTTP: its peer id changes whenever the
241
+ // process restarts, so a baked /p2p/<id> would rot. A libp2p multiaddr still
242
+ // takes the native path.
243
+ if (addr.startsWith('http://') || addr.startsWith('https://')) {
244
+ try {
245
+ const res = await fetch(`${addr.replace(/\/$/, '')}/peers`);
246
+ if (!res.ok)
247
+ return [];
248
+ const body = (await res.json());
249
+ return Array.isArray(body.peers)
250
+ ? body.peers.filter((p) => typeof p === 'string' && p.startsWith('/'))
251
+ : [];
252
+ }
253
+ catch {
254
+ return [];
255
+ }
256
+ }
219
257
  const s = await node.dialProtocol(multiaddr(addr), '/genkaku/peers/0');
220
258
  const reader = new FrameReader();
221
259
  for await (const chunk of s.source) {
@@ -263,4 +301,29 @@ export async function requestSettleCosign(node, addr, args) {
263
301
  }
264
302
  return { ok: false, sig: null, reason: reply?.reason ?? 'worker did not co-sign' };
265
303
  }
304
+ /**
305
+ * Tell a seed which addresses this worker can be reached on.
306
+ *
307
+ * Without this the seed can only answer with itself, which is no help to a cold
308
+ * client. A seed that does not implement the protocol is ignored.
309
+ */
310
+ export async function announceToSeed(node, seed, addrs) {
311
+ if (addrs.length === 0)
312
+ return false;
313
+ const s = await node.dialProtocol(multiaddr(seed), '/genkaku/announce/0');
314
+ try {
315
+ s.sink([lpEncode({ proto: 'announce/0', addrs })]);
316
+ const reader = new FrameReader();
317
+ for await (const chunk of s.source) {
318
+ reader.push(chunk);
319
+ const f = reader.next();
320
+ if (f?.proto === 'announced/0')
321
+ return f.ok === true;
322
+ }
323
+ return false;
324
+ }
325
+ finally {
326
+ await s.close().catch(() => { });
327
+ }
328
+ }
266
329
  //# sourceMappingURL=dial.js.map
@@ -1,3 +1,4 @@
1
+ import { type ModelProfile, type OllamaModelFacts } from './capabilities.ts';
1
2
  export type CompletionRequest = {
2
3
  model: string;
3
4
  prompt: string;
@@ -30,5 +31,70 @@ export declare function ollamaAdmin(host?: string): {
30
31
  status: string;
31
32
  }>;
32
33
  };
34
+ /**
35
+ * Ask Ollama what a model can do.
36
+ *
37
+ * `/api/show` is the authoritative answer for capabilities; the parameter size
38
+ * lives in `/api/tags` under `details.parameter_size`. Both are needed for the
39
+ * tier, so callers usually want {@link probeModels}.
40
+ *
41
+ * @param model - model name as Ollama knows it.
42
+ * @param host - Ollama base URL.
43
+ */
44
+ export declare function probeModel(model: string, host?: string): Promise<OllamaModelFacts>;
45
+ /**
46
+ * Profile every model Ollama has locally: capabilities from `/api/show`,
47
+ * size from `/api/tags`.
48
+ *
49
+ * A model whose probe fails is reported with no capabilities, which lands it in
50
+ * the `chat` tier. That is the safe direction: an unreadable model is never
51
+ * advertised as agent-capable.
52
+ *
53
+ * @param host - Ollama base URL.
54
+ */
55
+ export declare function probeModels(host?: string): Promise<ModelProfile[]>;
33
56
  export declare function createOllamaSource(host?: string): LocalCompletionSource;
57
+ /** One chat turn as the network carries it. */
58
+ export type ChatMessage = {
59
+ role: 'system' | 'user' | 'assistant' | 'tool';
60
+ content: string;
61
+ /** assistant turns only: calls the model already made */
62
+ tool_calls?: unknown[];
63
+ /** tool turns only: which call this answers */
64
+ tool_call_id?: string;
65
+ };
66
+ /** Incremental output of one chat completion. */
67
+ export type ChatEvent = {
68
+ type: 'text';
69
+ text: string;
70
+ } | {
71
+ type: 'tool-call';
72
+ index: number;
73
+ id?: string;
74
+ name?: string;
75
+ argumentsDelta: string;
76
+ };
77
+ /** A worker-side source that can serve a chat with tool schemas. */
78
+ export type LocalChatSource = {
79
+ streamChat(req: {
80
+ model: string;
81
+ messages: ChatMessage[];
82
+ tools?: unknown[];
83
+ maxTokens?: number;
84
+ }, onEvent: (event: ChatEvent) => void, signal?: AbortSignal): Promise<{
85
+ completionTokens: number;
86
+ model: string;
87
+ }>;
88
+ };
89
+ /**
90
+ * Serve a chat with tools through Ollama's `/api/chat`.
91
+ *
92
+ * The legacy `/api/generate` path takes a single prompt string and therefore
93
+ * cannot carry tool schemas or tool results; this source exists so a worker can
94
+ * serve an agent, not only a chat box.
95
+ *
96
+ * Ollama reports tool calls as complete objects rather than deltas, so each one
97
+ * is emitted as a single `tool-call` event with the full arguments string.
98
+ */
99
+ export declare function createOllamaChatSource(host?: string): LocalChatSource;
34
100
  //# sourceMappingURL=llmBridge.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"llmBridge.d.ts","sourceRoot":"","sources":["../src/llmBridge.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,2DAA2D;IAC3D,MAAM,CACJ,GAAG,EAAE,iBAAiB,EACtB,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,EAC/B,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC9E,CAAA;AAOD,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC9B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3B,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAClC,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAA;AAE3E,wBAAgB,WAAW,CAAC,IAAI,GAAE,MAAwD;IA6BtF,uEAAuE;YACzD,OAAO,CAAC,SAAS,EAAE,CAAC;IAmBlC,iFAAiF;gBAC/D,MAAM,eAAe,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,WAAW,WAAW,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;EAwBvH;AAKD,wBAAgB,kBAAkB,CAAC,IAAI,GAAE,MAAwD,GAAG,qBAAqB,CA0ExH"}
1
+ {"version":3,"file":"llmBridge.d.ts","sourceRoot":"","sources":["../src/llmBridge.ts"],"names":[],"mappings":"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"}
package/dist/llmBridge.js CHANGED
@@ -7,6 +7,7 @@
7
7
  // Python relay — the sidecar stays a thin, testable layer; a future variant can
8
8
  // point it at a hermes gateway's internal completion API instead (the interface
9
9
  // below is the seam: LocalCompletionSource).
10
+ import { profileOf } from "./capabilities.js";
10
11
  export function ollamaAdmin(host = process.env.GENKAKU_OLLAMA_HOST ?? DEFAULT_HOST) {
11
12
  async function readNdjson(res, onFrame) {
12
13
  if (!res.ok || !res.body) {
@@ -75,6 +76,62 @@ export function ollamaAdmin(host = process.env.GENKAKU_OLLAMA_HOST ?? DEFAULT_HO
75
76
  };
76
77
  }
77
78
  const DEFAULT_HOST = 'http://127.0.0.1:11434';
79
+ /**
80
+ * Ask Ollama what a model can do.
81
+ *
82
+ * `/api/show` is the authoritative answer for capabilities; the parameter size
83
+ * lives in `/api/tags` under `details.parameter_size`. Both are needed for the
84
+ * tier, so callers usually want {@link probeModels}.
85
+ *
86
+ * @param model - model name as Ollama knows it.
87
+ * @param host - Ollama base URL.
88
+ */
89
+ export async function probeModel(model, host = process.env.GENKAKU_OLLAMA_HOST ?? DEFAULT_HOST) {
90
+ const res = await fetch(new URL('/api/show', host), {
91
+ method: 'POST',
92
+ headers: { 'content-type': 'application/json' },
93
+ body: JSON.stringify({ model }),
94
+ });
95
+ if (!res.ok) {
96
+ throw new Error(`[genkaku:ollama] /api/show refused for ${model} (${res.status})`);
97
+ }
98
+ const body = (await res.json());
99
+ return {
100
+ ...(Array.isArray(body.capabilities) ? { capabilities: body.capabilities } : {}),
101
+ ...(body.details?.parameter_size === undefined ? {} : { parameterSize: body.details.parameter_size }),
102
+ };
103
+ }
104
+ /**
105
+ * Profile every model Ollama has locally: capabilities from `/api/show`,
106
+ * size from `/api/tags`.
107
+ *
108
+ * A model whose probe fails is reported with no capabilities, which lands it in
109
+ * the `chat` tier. That is the safe direction: an unreadable model is never
110
+ * advertised as agent-capable.
111
+ *
112
+ * @param host - Ollama base URL.
113
+ */
114
+ export async function probeModels(host = process.env.GENKAKU_OLLAMA_HOST ?? DEFAULT_HOST) {
115
+ const res = await fetch(new URL('/api/tags', host), { method: 'GET' });
116
+ if (!res.ok) {
117
+ throw new Error(`[genkaku:ollama] /api/tags refused (${res.status}) — is Ollama running?`);
118
+ }
119
+ const body = (await res.json());
120
+ const rows = body.models ?? [];
121
+ return Promise.all(rows.map(async (row) => {
122
+ let capabilities;
123
+ try {
124
+ capabilities = (await probeModel(row.name, host)).capabilities;
125
+ }
126
+ catch {
127
+ capabilities = undefined;
128
+ }
129
+ return profileOf(row.name, {
130
+ ...(capabilities === undefined ? {} : { capabilities }),
131
+ ...(row.details?.parameter_size === undefined ? {} : { parameterSize: row.details.parameter_size }),
132
+ });
133
+ }));
134
+ }
78
135
  export function createOllamaSource(host = process.env.GENKAKU_OLLAMA_HOST ?? DEFAULT_HOST) {
79
136
  return {
80
137
  async stream(req, onDelta, signal) {
@@ -148,4 +205,80 @@ export function createOllamaSource(host = process.env.GENKAKU_OLLAMA_HOST ?? DEF
148
205
  },
149
206
  };
150
207
  }
208
+ /**
209
+ * Serve a chat with tools through Ollama's `/api/chat`.
210
+ *
211
+ * The legacy `/api/generate` path takes a single prompt string and therefore
212
+ * cannot carry tool schemas or tool results; this source exists so a worker can
213
+ * serve an agent, not only a chat box.
214
+ *
215
+ * Ollama reports tool calls as complete objects rather than deltas, so each one
216
+ * is emitted as a single `tool-call` event with the full arguments string.
217
+ */
218
+ export function createOllamaChatSource(host = process.env.GENKAKU_OLLAMA_HOST ?? DEFAULT_HOST) {
219
+ return {
220
+ async streamChat(req, onEvent, signal) {
221
+ const res = await fetch(new URL('/api/chat', host), {
222
+ method: 'POST',
223
+ headers: { 'content-type': 'application/json' },
224
+ body: JSON.stringify({
225
+ model: req.model,
226
+ messages: req.messages,
227
+ stream: true,
228
+ ...(req.tools === undefined || req.tools.length === 0 ? {} : { tools: req.tools }),
229
+ options: {
230
+ num_ctx: Number(process.env.GENKAKU_OLLAMA_NUM_CTX ?? 8192),
231
+ ...(req.maxTokens ? { num_predict: req.maxTokens } : {}),
232
+ },
233
+ }),
234
+ ...(signal === undefined ? {} : { signal }),
235
+ });
236
+ if (!res.ok) {
237
+ throw new Error(`[genkaku:ollama] /api/chat refused (${res.status}) — is Ollama running?`);
238
+ }
239
+ let completionTokens = 0;
240
+ let toolIndex = 0;
241
+ const handleFrame = (frame) => {
242
+ const message = frame.message;
243
+ if (typeof message?.content === 'string' && message.content !== '') {
244
+ completionTokens += 1;
245
+ onEvent({ type: 'text', text: message.content });
246
+ }
247
+ for (const call of message?.tool_calls ?? []) {
248
+ const args = call.function?.arguments;
249
+ onEvent({
250
+ type: 'tool-call',
251
+ index: toolIndex++,
252
+ name: call.function?.name ?? '',
253
+ argumentsDelta: typeof args === 'string' ? args : JSON.stringify(args ?? {}),
254
+ });
255
+ }
256
+ };
257
+ const reader = res.body?.getReader();
258
+ if (reader == null)
259
+ throw new Error('[genkaku:ollama] /api/chat returned no body');
260
+ const decoder = new TextDecoder();
261
+ let buffer = '';
262
+ for (;;) {
263
+ const { done, value } = await reader.read();
264
+ if (done)
265
+ break;
266
+ buffer += decoder.decode(value, { stream: true });
267
+ let nl = buffer.indexOf('\n');
268
+ while (nl >= 0) {
269
+ const line = buffer.slice(0, nl).trim();
270
+ buffer = buffer.slice(nl + 1);
271
+ if (line !== '') {
272
+ try {
273
+ handleFrame(JSON.parse(line));
274
+ }
275
+ catch { /* tolerate */ }
276
+ }
277
+ nl = buffer.indexOf('\n');
278
+ }
279
+ }
280
+ return { completionTokens, model: req.model };
281
+ },
282
+ };
283
+ }
151
284
  //# sourceMappingURL=llmBridge.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"netconfig.d.ts","sourceRoot":"","sources":["../src/netconfig.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,cAAc,GAAG,cAAc,GAAG,MAAM,CAAA;AAEpD,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,cAAc,EAAE,KAAK,GAAG,IAAI,CAGzD,CAAA;AAED,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,cAAc,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAWhG,CAAA;AAED,eAAO,MAAM,QAAQ,+CAA+C,CAAA;AAWpE,eAAO,MAAM,eAAe,EAAE,MAAM,EAQhC,CAAA;AAGJ,eAAO,MAAM,2BAA2B,KAAK,CAAA"}
1
+ {"version":3,"file":"netconfig.d.ts","sourceRoot":"","sources":["../src/netconfig.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,cAAc,GAAG,cAAc,GAAG,MAAM,CAAA;AAEpD,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,cAAc,EAAE,KAAK,GAAG,IAAI,CAGzD,CAAA;AAED,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,cAAc,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAWhG,CAAA;AAED,eAAO,MAAM,QAAQ,+CAA+C,CAAA;AAgBpE,eAAO,MAAM,eAAe,EAAE,MAAM,EAQhC,CAAA;AAGJ,eAAO,MAAM,2BAA2B,KAAK,CAAA"}
package/dist/netconfig.js CHANGED
@@ -25,7 +25,12 @@ export const TREASURY = '0x639a2B713041e15B0c5C18710536b661d5265b48';
25
25
  // `GENKAKU_BOOTSTRAP_SEEDS` (comma-separated multiaddrs) is a DEVELOPMENT
26
26
  // escape hatch for a LAN bring-up or a private deployment; it is empty in
27
27
  // shipped builds and is never required for discovery to work.
28
- const BAKED_SEEDS = [];
28
+ // The live GenKaKu bootstrap seed. It is addressed by URL rather than by
29
+ // multiaddr because the process picks a new peer id on every restart; the URL
30
+ // answers with the addresses that are true right now.
31
+ const BAKED_SEEDS = [
32
+ 'https://staging.genkaku.app/seed',
33
+ ];
29
34
  export const HARDCODED_SEEDS = (() => {
30
35
  const fromEnv = (process.env.GENKAKU_BOOTSTRAP_SEEDS ?? '')
31
36
  .split(',')
package/dist/offer.d.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  export type Offer = {
2
- proto: 'offers/0';
2
+ /**
3
+ * Board revision this row was read from. `offers/1` adds the capability
4
+ * fields below; a v0 row is still valid and is treated as a chat-only model,
5
+ * because a worker that cannot report capabilities cannot be trusted to
6
+ * support tools either.
7
+ */
8
+ proto: 'offers/0' | 'offers/1';
3
9
  workerName: string;
4
10
  model: string;
5
11
  /** 6-decimal USDC per hour of usage, as bigint */
@@ -17,6 +23,18 @@ export type Offer = {
17
23
  latencyMs?: number;
18
24
  /** parallel sessions the worker accepts */
19
25
  maxConcurrent?: number;
26
+ /**
27
+ * Ollama's capability list for the model being sold (`tools`, `thinking`,
28
+ * `vision`, …). Present on `offers/1` rows.
29
+ */
30
+ capabilities?: string[];
31
+ /** raw `details.parameter_size`, e.g. '9.0B'. Present on `offers/1` rows. */
32
+ parameterSize?: string;
33
+ /**
34
+ * Marketplace tier derived from capabilities + size. `chat` is what the UI
35
+ * labels "not suitable for agents"; it is a label, not a filter.
36
+ */
37
+ tier?: ModelTier;
20
38
  /** true when `workerWallet` was proven by the P4.9e hello handshake rather
21
39
  * than self-reported. Priced sessions should require this. */
22
40
  walletVerified?: boolean;
@@ -42,6 +60,7 @@ export declare function ceilDiv(a: bigint, b: bigint): bigint;
42
60
  * the quoted pricePerHour, and the worker's per-tick exposure stays bounded.
43
61
  */
44
62
  export declare function cumulativeBilled(pricePerHour: bigint, unitMs: number, elapsedMs: number): bigint;
63
+ import type { ModelTier } from './capabilities.ts';
45
64
  export type OfferSortKey = 'creditScore' | 'pricePerHour' | 'workerName' | 'model' | 'city' | 'latencyMs';
46
65
  /** One offer row with the address it arrived from. */
47
66
  export type AddressedOffer = {
@@ -1 +1 @@
1
- {"version":3,"file":"offer.d.ts","sourceRoot":"","sources":["../src/offer.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,KAAK,GAAG;IAClB,KAAK,EAAE,UAAU,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,kDAAkD;IAClD,YAAY,EAAE,MAAM,CAAA;IACpB,0DAA0D;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,wDAAwD;IACxD,WAAW,EAAE,MAAM,CAAA;IACnB,+DAA+D;IAC/D,cAAc,EAAE,MAAM,CAAA;IACtB;mEAC+D;IAC/D,YAAY,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;IAC/B,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,2CAA2C;IAC3C,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;mEAC+D;IAC/D,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,0DAA0D;AAC1D,MAAM,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,EAAE,cAAc,CAAC,GAAG;IAAE,YAAY,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9E,wBAAgB,MAAM,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,CAE9C;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,KAAK,CAE/C;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAIpD;AAID;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAahG;AAED,MAAM,MAAM,YAAY,GAAG,aAAa,GAAG,cAAc,GAAG,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,WAAW,CAAA;AAWzG,sDAAsD;AACtD,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,CAAA;AAE3D;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,cAAc,EAAE,GAAG,cAAc,EAAE,CAW9E;AAED,uEAAuE;AACvE,wBAAgB,UAAU,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,GAAG,GAAE,YAA4B,EAAE,GAAG,GAAE,KAAK,GAAG,MAAe,GAAG,KAAK,EAAE,CAOpH;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,EAAE,WAAW,GAAG,KAAK,EAAE,CASrE"}
1
+ {"version":3,"file":"offer.d.ts","sourceRoot":"","sources":["../src/offer.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,KAAK,GAAG;IAClB;;;;;OAKG;IACH,KAAK,EAAE,UAAU,GAAG,UAAU,CAAA;IAC9B,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,kDAAkD;IAClD,YAAY,EAAE,MAAM,CAAA;IACpB,0DAA0D;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,wDAAwD;IACxD,WAAW,EAAE,MAAM,CAAA;IACnB,+DAA+D;IAC/D,cAAc,EAAE,MAAM,CAAA;IACtB;mEAC+D;IAC/D,YAAY,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;IAC/B,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,2CAA2C;IAC3C,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;OAGG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB;mEAC+D;IAC/D,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,0DAA0D;AAC1D,MAAM,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,EAAE,cAAc,CAAC,GAAG;IAAE,YAAY,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9E,wBAAgB,MAAM,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,CAE9C;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,KAAK,CAI/C;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAIpD;AAID;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAahG;AAED,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAElD,MAAM,MAAM,YAAY,GAAG,aAAa,GAAG,cAAc,GAAG,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,WAAW,CAAA;AAWzG,sDAAsD;AACtD,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,CAAA;AAE3D;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,cAAc,EAAE,GAAG,cAAc,EAAE,CAW9E;AAED,uEAAuE;AACvE,wBAAgB,UAAU,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,GAAG,GAAE,YAA4B,EAAE,GAAG,GAAE,KAAK,GAAG,MAAe,GAAG,KAAK,EAAE,CAOpH;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,EAAE,WAAW,GAAG,KAAK,EAAE,CASrE"}
package/dist/offer.js CHANGED
@@ -9,7 +9,9 @@ export function toWire(offer) {
9
9
  return { ...offer, pricePerHour: offer.pricePerHour.toString() };
10
10
  }
11
11
  export function fromWire(wire) {
12
- return { ...wire, pricePerHour: BigInt(wire.pricePerHour), proto: 'offers/0' };
12
+ // Preserve the revision the row arrived with: a v0 row must not be upgraded
13
+ // silently, or it would look like a worker that reported capabilities.
14
+ return { ...wire, pricePerHour: BigInt(wire.pricePerHour) };
13
15
  }
14
16
  export function ceilDiv(a, b) {
15
17
  if (b <= 0n) {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=offer.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offer.test.d.ts","sourceRoot":"","sources":["../src/offer.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,33 @@
1
+ import assert from 'node:assert/strict';
2
+ import { test } from 'node:test';
3
+ import { fromWire, toWire } from "./offer.js";
4
+ const base = {
5
+ proto: 'offers/1',
6
+ workerName: 'worker-a',
7
+ model: 'qwen3.8:27b',
8
+ pricePerHour: 6000000n,
9
+ creditScore: 80,
10
+ sessionsServed: 3,
11
+ capabilities: ['completion', 'tools'],
12
+ parameterSize: '27B',
13
+ tier: 'agent',
14
+ };
15
+ test('offers/1 survives the wire round trip', () => {
16
+ const wire = toWire(base);
17
+ assert.equal(wire.pricePerHour, '6000000');
18
+ assert.deepEqual(fromWire(wire), base);
19
+ });
20
+ test('a v0 row stays v0 after decoding, so it cannot pose as capability-aware', () => {
21
+ const v0 = {
22
+ proto: 'offers/0',
23
+ workerName: 'old-worker',
24
+ model: 'llama3:8b',
25
+ pricePerHour: 1000000n,
26
+ creditScore: 10,
27
+ sessionsServed: 0,
28
+ };
29
+ const decoded = fromWire(toWire(v0));
30
+ assert.equal(decoded.proto, 'offers/0');
31
+ assert.equal(decoded.tier, undefined);
32
+ });
33
+ //# sourceMappingURL=offer.test.js.map
package/dist/p2pNode.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type Libp2p } from 'libp2p';
2
2
  import { type Hex } from 'viem';
3
3
  import type { Paywall } from './paywall.ts';
4
- import type { LocalCompletionSource } from './llmBridge.ts';
4
+ import type { LocalChatSource, LocalCompletionSource } from './llmBridge.ts';
5
5
  import type { ModeManager } from './modeManager.ts';
6
6
  import { type Offer } from './offer.ts';
7
7
  import type { SessionEvent } from './workerSession.ts';
@@ -11,6 +11,8 @@ export declare const GENKAKU_PROTOCOLS: readonly ["/genkaku/open/0", "/genkaku/t
11
11
  export type SidecarOptions = {
12
12
  modeManager: ModeManager;
13
13
  source: LocalCompletionSource;
14
+ /** Serves `open/1` chat requests (messages + tool schemas). */
15
+ chatSource?: LocalChatSource;
14
16
  /** ip4/tcp listen addrs; LAN first, NAT traversal (hole punching) later */
15
17
  listen: string[];
16
18
  /** one Offer row per (model, price) the worker publishes — visible to users */
@@ -1 +1 @@
1
- {"version":3,"file":"p2pNode.d.ts","sourceRoot":"","sources":["../src/p2pNode.ts"],"names":[],"mappings":"AAYA,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAA;AAClD,OAAO,EAAE,KAAK,GAAG,EAAW,MAAM,MAAM,CAAA;AAIxC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAE3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAGnD,OAAO,EAAoB,KAAK,KAAK,EAAU,MAAM,YAAY,CAAA;AAajE,OAAO,KAAK,EAAkB,YAAY,EAAiB,MAAM,oBAAoB,CAAA;AAGrF,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AACzD,YAAY,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAExC,eAAO,MAAM,iBAAiB,2JAQpB,CAAA;AAEV,MAAM,MAAM,cAAc,GAAG;IAC3B,WAAW,EAAE,WAAW,CAAA;IACxB,MAAM,EAAE,qBAAqB,CAAA;IAC7B,2EAA2E;IAC3E,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,+EAA+E;IAC/E,OAAO,EAAE,KAAK,EAAE,CAAA;IAChB,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,CAAA;IACzC,gFAAgF;IAChF,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;kFAC8E;IAC9E,SAAS,CAAC,EAAE,GAAG,CAAA;CAChB,CAAA;AAED;0EAC0E;AAC1E,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,CAAA;IAChB,kEAAkE;IAClE,SAAS,EAAE,KAAK,MAAM,EAAE,CAAA;IACxB,UAAU,EAAE,KAAK,MAAM,EAAE,CAAA;CAC1B,CAAA;AAkBD,+EAA+E;AAC/E,wBAAsB,cAAc,CAAC,MAAM,GAAE,MAAM,EAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAY3E;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,MAAM,EAAE,CAAA;IACzB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACrB,cAAc,EAAE,MAAM,MAAM,CAAA;IAC5B,+DAA+D;IAC/D,SAAS,EAAE,MAAM,MAAM,CAAA;IACvB,qEAAqE;IACrE,UAAU,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,IAAI,CAAA;CACtC,CAAA;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CA8ejF"}
1
+ {"version":3,"file":"p2pNode.d.ts","sourceRoot":"","sources":["../src/p2pNode.ts"],"names":[],"mappings":"AAYA,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAA;AAClD,OAAO,EAAE,KAAK,GAAG,EAAW,MAAM,MAAM,CAAA;AAIxC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,KAAK,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAE5E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAGnD,OAAO,EAAoB,KAAK,KAAK,EAAU,MAAM,YAAY,CAAA;AAajE,OAAO,KAAK,EAAgB,YAAY,EAAiB,MAAM,oBAAoB,CAAA;AAGnF,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AACzD,YAAY,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAExC,eAAO,MAAM,iBAAiB,2JAQpB,CAAA;AAEV,MAAM,MAAM,cAAc,GAAG;IAC3B,WAAW,EAAE,WAAW,CAAA;IACxB,MAAM,EAAE,qBAAqB,CAAA;IAC7B,+DAA+D;IAC/D,UAAU,CAAC,EAAE,eAAe,CAAA;IAC5B,2EAA2E;IAC3E,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,+EAA+E;IAC/E,OAAO,EAAE,KAAK,EAAE,CAAA;IAChB,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,CAAA;IACzC,gFAAgF;IAChF,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;kFAC8E;IAC9E,SAAS,CAAC,EAAE,GAAG,CAAA;CAChB,CAAA;AAED;0EAC0E;AAC1E,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,CAAA;IAChB,kEAAkE;IAClE,SAAS,EAAE,KAAK,MAAM,EAAE,CAAA;IACxB,UAAU,EAAE,KAAK,MAAM,EAAE,CAAA;CAC1B,CAAA;AAkBD,+EAA+E;AAC/E,wBAAsB,cAAc,CAAC,MAAM,GAAE,MAAM,EAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAY3E;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,MAAM,EAAE,CAAA;IACzB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACrB,cAAc,EAAE,MAAM,MAAM,CAAA;IAC5B,+DAA+D;IAC/D,SAAS,EAAE,MAAM,MAAM,CAAA;IACvB,qEAAqE;IACrE,UAAU,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,IAAI,CAAA;CACtC,CAAA;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAkfjF"}
package/dist/p2pNode.js CHANGED
@@ -128,7 +128,7 @@ export async function createSidecar(opts) {
128
128
  break;
129
129
  }
130
130
  }
131
- if (!env || env.proto !== 'open/0') {
131
+ if (!env || (env.proto !== 'open/0' && env.proto !== 'open/1')) {
132
132
  await stream.close();
133
133
  return;
134
134
  }
@@ -185,6 +185,10 @@ export async function createSidecar(opts) {
185
185
  const session = await import("./workerSession.js").then(({ createWorkerSession }) => createWorkerSession(env, {
186
186
  modeManager: opts.modeManager,
187
187
  source: opts.source,
188
+ ...(opts.chatSource === undefined ? {} : { chatSource: opts.chatSource }),
189
+ onToolCall: (call) => {
190
+ dq.push(lpEncode({ proto: 'tool/0', ...call }));
191
+ },
188
192
  ledger,
189
193
  emit,
190
194
  onDelta: (delta) => {
@@ -1,4 +1,5 @@
1
1
  import type { WorkerTransport } from './dial.ts';
2
+ import type { ChatMessage } from './llmBridge.ts';
2
3
  import type { ModeManager } from './modeManager.ts';
3
4
  import { type NetEnvironment } from './netconfig.ts';
4
5
  import type { Offer } from './offer.ts';
@@ -12,6 +13,14 @@ export type UserSessionEvent = {
12
13
  } | {
13
14
  type: 'token';
14
15
  delta: string;
16
+ }
17
+ /** A tool call produced by the model, relayed by an open/1 worker. */
18
+ | {
19
+ type: 'tool-delta';
20
+ index: number;
21
+ id: string;
22
+ name: string;
23
+ argumentsDelta: string;
15
24
  } | {
16
25
  type: 'receipt-sent';
17
26
  seq: number;
@@ -34,6 +43,10 @@ export type UserSessionOptions = {
34
43
  prompt: string;
35
44
  system?: string;
36
45
  maxTokens?: number;
46
+ /** Whole conversation; when present the session opens as open/1. */
47
+ messages?: ChatMessage[];
48
+ /** JSON schemas the model may call. Requires `messages`. */
49
+ tools?: unknown[];
37
50
  transport: WorkerTransport;
38
51
  modeManager: ModeManager;
39
52
  onToken?: (delta: string) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"userSession.d.ts","sourceRoot":"","sources":["../src/userSession.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACnD,OAAO,EAAuB,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACzE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAEvC,OAAO,EAKL,KAAK,gBAAgB,EAEtB,MAAM,eAAe,CAAA;AAGtB,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAC5F;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACvF;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACzE;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9C,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,+DAA+D;IAC/D,KAAK,EAAE,KAAK,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,eAAe,CAAA;IAC1B,WAAW,EAAE,WAAW,CAAA;IACxB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,gBAAgB,KAAK,IAAI,CAAA;IACpC,wDAAwD;IACxD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,mEAAmE;IACnE,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;CAC7D,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,wEAAwE;IACxE,OAAO,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAC7C,CAAA;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAwHzF"}
1
+ {"version":3,"file":"userSession.d.ts","sourceRoot":"","sources":["../src/userSession.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACnD,OAAO,EAAuB,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACzE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAEvC,OAAO,EAKL,KAAK,gBAAgB,EAEtB,MAAM,eAAe,CAAA;AAGtB,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAC5F;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AAClC,sEAAsE;GACpE;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,GACvF;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACvF;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACzE;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9C,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,+DAA+D;IAC/D,KAAK,EAAE,KAAK,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAA;IACxB,4DAA4D;IAC5D,KAAK,CAAC,EAAE,OAAO,EAAE,CAAA;IACjB,SAAS,EAAE,eAAe,CAAA;IAC1B,WAAW,EAAE,WAAW,CAAA;IACxB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,gBAAgB,KAAK,IAAI,CAAA;IACpC,wDAAwD;IACxD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,mEAAmE;IACnE,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;CAC7D,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,wEAAwE;IACxE,OAAO,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAC7C,CAAA;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA+HzF"}
@@ -30,8 +30,7 @@ export async function runUserSession(opts) {
30
30
  });
31
31
  let output = '';
32
32
  try {
33
- const envelope = {
34
- proto: 'open/0',
33
+ const common = {
35
34
  jobId: opts.jobId,
36
35
  escrowId,
37
36
  model: opts.offer.model,
@@ -41,8 +40,16 @@ export async function runUserSession(opts) {
41
40
  clientPeer: 'user', // replaced by the real peer id at app wiring
42
41
  billing: { mode: 'minutes', unitMs, pricePerHour: opts.offer.pricePerHour.toString(), currency: 'USDC' },
43
42
  };
43
+ // A conversation with tool schemas is served by Ollama's chat API; the
44
+ // single-prompt form cannot carry either.
45
+ const envelope = opts.messages == null
46
+ ? { proto: 'open/0', ...common }
47
+ : { proto: 'open/1', ...common, messages: opts.messages, ...(opts.tools === undefined ? {} : { tools: opts.tools }) };
44
48
  emit({ type: 'session-open', jobId: opts.jobId, worker: opts.offer.workerName, model: opts.offer.model, pricePerHour: opts.offer.pricePerHour });
45
49
  await opts.transport.open(envelope);
50
+ opts.transport.onToolCall((call) => {
51
+ emit({ type: 'tool-delta', ...call });
52
+ });
46
53
  opts.transport.onToken((delta) => {
47
54
  tokens += 1;
48
55
  output += delta;
@@ -1,4 +1,4 @@
1
- import type { LocalCompletionSource } from './llmBridge.ts';
1
+ import type { ChatMessage, LocalChatSource, LocalCompletionSource } from './llmBridge.ts';
2
2
  import type { ModeManager } from './modeManager.ts';
3
3
  import type { ReceiptLedger } from './receipts.ts';
4
4
  export type OpenEnvelopeV0 = {
@@ -24,6 +24,17 @@ export type OpenEnvelopeV0 = {
24
24
  currency: 'USDC';
25
25
  };
26
26
  };
27
+ /**
28
+ * Chat revision: carries whole messages and tool schemas, which `/api/generate`
29
+ * cannot express. A worker that receives this serves through Ollama's
30
+ * `/api/chat` so an agent can actually call tools.
31
+ */
32
+ export type OpenEnvelopeV1 = Omit<OpenEnvelopeV0, 'proto'> & {
33
+ proto: 'open/1';
34
+ messages: ChatMessage[];
35
+ tools?: unknown[];
36
+ };
37
+ export type OpenEnvelope = OpenEnvelopeV0 | OpenEnvelopeV1;
27
38
  export type SessionEvent = {
28
39
  type: 'session-open';
29
40
  sessionId: string;
@@ -47,6 +58,15 @@ export type SessionEvent = {
47
58
  export type WorkerSessionDeps = {
48
59
  modeManager: ModeManager;
49
60
  source: LocalCompletionSource;
61
+ /** Required to serve `open/1`; absent means chat requests are refused. */
62
+ chatSource?: LocalChatSource;
63
+ /** Relay one tool call produced by the model. */
64
+ onToolCall?: (call: {
65
+ index: number;
66
+ id: string;
67
+ name: string;
68
+ argumentsDelta: string;
69
+ }) => void;
50
70
  ledger: ReceiptLedger;
51
71
  emit: (e: SessionEvent) => void;
52
72
  onDelta: (delta: string) => void;
@@ -63,5 +83,5 @@ export type WorkerSession = {
63
83
  abort(): void;
64
84
  run(): Promise<void>;
65
85
  };
66
- export declare function createWorkerSession(envelope: OpenEnvelopeV0, deps: WorkerSessionDeps): WorkerSession;
86
+ export declare function createWorkerSession(envelope: OpenEnvelope, deps: WorkerSessionDeps): WorkerSession;
67
87
  //# sourceMappingURL=workerSession.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"workerSession.d.ts","sourceRoot":"","sources":["../src/workerSession.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAqB,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAC9E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACnD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAElD,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,QAAQ,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,8EAA8E;IAC9E,UAAU,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,SAAS,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAA;CACtF,CAAA;AAED,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC5E;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC1D;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9C,MAAM,MAAM,iBAAiB,GAAG;IAC9B,WAAW,EAAE,WAAW,CAAA;IACxB,MAAM,EAAE,qBAAqB,CAAA;IAC7B,MAAM,EAAE,aAAa,CAAA;IACrB,IAAI,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,CAAA;IAC/B,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,4EAA4E;IAC5E,aAAa,EAAE,MAAM;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IAC3D,WAAW,CAAC,EAAE,WAAW,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,MAAM,CAAA;IACpB,KAAK,IAAI,IAAI,CAAA;IACb,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACrB,CAAA;AAED,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,cAAc,EACxB,IAAI,EAAE,iBAAiB,GACtB,aAAa,CAmEf"}
1
+ {"version":3,"file":"workerSession.d.ts","sourceRoot":"","sources":["../src/workerSession.ts"],"names":[],"mappings":"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"}
@@ -39,16 +39,48 @@ export function createWorkerSession(envelope, deps) {
39
39
  system: envelope.system,
40
40
  maxTokens: envelope.maxTokens,
41
41
  };
42
+ const chat = envelope.proto === 'open/1' ? envelope : null;
42
43
  try {
43
- await deps.source.stream(req, (delta) => {
44
- if (aborted) {
45
- return;
44
+ if (chat != null) {
45
+ if (deps.chatSource == null) {
46
+ throw new Error('worker cannot serve open/1: no chat source configured');
46
47
  }
47
- tokens += 1; // one delta ≈ one token frame from the local source
48
- deps.emit({ type: 'token-count', tokens });
49
- deps.onDelta(delta);
50
- checkReceipts();
51
- }, deps.abortSignal ?? undefined);
48
+ await deps.chatSource.streamChat({
49
+ model: chat.model,
50
+ messages: chat.messages,
51
+ ...(chat.tools === undefined ? {} : { tools: chat.tools }),
52
+ ...(chat.maxTokens === undefined ? {} : { maxTokens: chat.maxTokens }),
53
+ }, (event) => {
54
+ if (aborted) {
55
+ return;
56
+ }
57
+ if (event.type === 'text') {
58
+ tokens += 1;
59
+ deps.emit({ type: 'token-count', tokens });
60
+ deps.onDelta(event.text);
61
+ }
62
+ else {
63
+ deps.onToolCall?.({
64
+ index: event.index,
65
+ id: event.id ?? `call_${event.index}`,
66
+ name: event.name ?? '',
67
+ argumentsDelta: event.argumentsDelta,
68
+ });
69
+ }
70
+ checkReceipts();
71
+ }, deps.abortSignal ?? undefined);
72
+ }
73
+ else {
74
+ await deps.source.stream(req, (delta) => {
75
+ if (aborted) {
76
+ return;
77
+ }
78
+ tokens += 1; // one delta ≈ one token frame from the local source
79
+ deps.emit({ type: 'token-count', tokens });
80
+ deps.onDelta(delta);
81
+ checkReceipts();
82
+ }, deps.abortSignal ?? undefined);
83
+ }
52
84
  deps.emit({ type: 'session-end', sessionId, tokens });
53
85
  }
54
86
  catch (err) {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=workerSession.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workerSession.test.d.ts","sourceRoot":"","sources":["../src/workerSession.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,49 @@
1
+ import assert from 'node:assert/strict';
2
+ import { test } from 'node:test';
3
+ import { createWorkerSession } from "./workerSession.js";
4
+ const envelope = {
5
+ proto: 'open/1',
6
+ jobId: 'j1',
7
+ escrowId: '0',
8
+ model: 'm',
9
+ prompt: 'hi',
10
+ clientPeer: 'peer',
11
+ messages: [{ role: 'user', content: 'hi' }],
12
+ tools: [{ name: 'read', description: 'read a file', parameters: {} }],
13
+ };
14
+ test('an open/1 session serves chat and relays tool calls', async () => {
15
+ const text = [];
16
+ const calls = [];
17
+ const session = createWorkerSession(envelope, {
18
+ modeManager: { current: () => ({ sharingActive: true }) },
19
+ source: { stream: async () => ({ promptTokens: 0, completionTokens: 0, model: 'm' }) },
20
+ chatSource: {
21
+ async streamChat(_req, onEvent) {
22
+ onEvent({ type: 'text', text: 'let me look' });
23
+ onEvent({ type: 'tool-call', index: 0, name: 'read', argumentsDelta: '{"path":"a"}' });
24
+ return { completionTokens: 3, model: 'm' };
25
+ },
26
+ },
27
+ ledger: { offer: () => ({ accepted: true }) },
28
+ emit: () => { },
29
+ onDelta: delta => text.push(delta),
30
+ onToolCall: call => calls.push(call),
31
+ latestReceipt: () => null,
32
+ });
33
+ await session.run();
34
+ assert.deepEqual(text, ['let me look']);
35
+ assert.deepEqual(calls, [{ index: 0, id: 'call_0', name: 'read', argumentsDelta: '{"path":"a"}' }]);
36
+ });
37
+ test('an open/1 session without a chat source fails loudly instead of answering empty', async () => {
38
+ const session = createWorkerSession(envelope, {
39
+ modeManager: { current: () => ({ sharingActive: true }) },
40
+ source: { stream: async () => ({ promptTokens: 0, completionTokens: 0, model: 'm' }) },
41
+ ledger: { offer: () => ({ accepted: true }) },
42
+ emit: () => { },
43
+ onDelta: () => { },
44
+ latestReceipt: () => null,
45
+ });
46
+ await session.run();
47
+ assert.equal(session.tokens(), 0);
48
+ });
49
+ //# sourceMappingURL=workerSession.test.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genkaku/protocol",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "GenKaKu protocol core: on-chain worker directory, USDC escrow billing, libp2p transport, Ollama lifecycle",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -54,6 +54,6 @@
54
54
  "scripts": {
55
55
  "build": "tsc -p tsconfig.json",
56
56
  "typecheck": "tsc -p tsconfig.json --noEmit",
57
- "test": "node --test dist/**/*.test.js"
57
+ "test": "tsc -p tsconfig.json && node --test dist/*.test.js"
58
58
  }
59
59
  }