@pikku/core 0.12.20 → 0.12.21

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.
Files changed (138) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/dev/hot-reload.js +20 -6
  3. package/dist/errors/error-handler.d.ts +1 -1
  4. package/dist/errors/error-handler.js +2 -2
  5. package/dist/function/functions.types.d.ts +3 -2
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.js +2 -2
  8. package/dist/middleware/index.d.ts +2 -2
  9. package/dist/middleware/index.js +2 -2
  10. package/dist/middleware/telemetry.d.ts +2 -2
  11. package/dist/middleware/telemetry.js +2 -2
  12. package/dist/middleware-runner.d.ts +15 -27
  13. package/dist/middleware-runner.js +25 -30
  14. package/dist/permissions.d.ts +15 -22
  15. package/dist/permissions.js +30 -25
  16. package/dist/pikku-request.js +1 -1
  17. package/dist/pikku-state.js +2 -3
  18. package/dist/services/ai-agent-runner-service.d.ts +1 -0
  19. package/dist/services/in-memory-queue-service.js +1 -1
  20. package/dist/services/in-memory-workflow-service.d.ts +15 -13
  21. package/dist/services/in-memory-workflow-service.js +31 -13
  22. package/dist/services/local-content.js +8 -1
  23. package/dist/services/user-session-service.d.ts +1 -1
  24. package/dist/testing/service-tests.js +24 -0
  25. package/dist/types/core.types.d.ts +4 -0
  26. package/dist/types/state.types.d.ts +2 -18
  27. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +24 -5
  28. package/dist/wirings/ai-agent/ai-agent-memory.js +128 -23
  29. package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +10 -1
  30. package/dist/wirings/ai-agent/ai-agent-model-config.js +15 -35
  31. package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -1
  32. package/dist/wirings/ai-agent/ai-agent-runner.js +45 -31
  33. package/dist/wirings/ai-agent/ai-agent-stream.js +63 -34
  34. package/dist/wirings/ai-agent/ai-agent.types.d.ts +20 -0
  35. package/dist/wirings/channel/channel-handler.js +1 -1
  36. package/dist/wirings/channel/channel-store.d.ts +13 -0
  37. package/dist/wirings/channel/channel.types.d.ts +3 -0
  38. package/dist/wirings/channel/local/local-channel-runner.js +23 -5
  39. package/dist/wirings/channel/pikku-abstract-channel-handler.js +8 -0
  40. package/dist/wirings/channel/serverless/serverless-channel-runner.js +9 -0
  41. package/dist/wirings/cli/cli-runner.js +24 -5
  42. package/dist/wirings/cli/command-parser.js +19 -4
  43. package/dist/wirings/http/http-runner.js +72 -36
  44. package/dist/wirings/http/http.types.d.ts +1 -0
  45. package/dist/wirings/http/pikku-fetch-http-request.js +3 -1
  46. package/dist/wirings/http/web-request.js +32 -0
  47. package/dist/wirings/rpc/rpc-runner.js +13 -3
  48. package/dist/wirings/workflow/graph/graph-node.d.ts +8 -0
  49. package/dist/wirings/workflow/graph/graph-node.js +4 -0
  50. package/dist/wirings/workflow/graph/graph-runner.js +12 -10
  51. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +4 -0
  52. package/dist/wirings/workflow/index.d.ts +1 -1
  53. package/dist/wirings/workflow/pikku-workflow-service.d.ts +81 -16
  54. package/dist/wirings/workflow/pikku-workflow-service.js +266 -45
  55. package/dist/wirings/workflow/workflow.types.d.ts +34 -0
  56. package/package.json +1 -1
  57. package/run-tests.sh +4 -1
  58. package/src/dev/hot-reload.test.ts +62 -11
  59. package/src/dev/hot-reload.ts +28 -7
  60. package/src/errors/error-handler.ts +8 -2
  61. package/src/errors/error.test.ts +2 -0
  62. package/src/function/function-runner.test.ts +500 -10
  63. package/src/function/functions.types.ts +3 -2
  64. package/src/index.ts +12 -2
  65. package/src/middleware/index.ts +11 -2
  66. package/src/middleware/telemetry.ts +2 -2
  67. package/src/middleware-runner.test.ts +16 -16
  68. package/src/middleware-runner.ts +42 -30
  69. package/src/permissions.test.ts +27 -24
  70. package/src/permissions.ts +41 -25
  71. package/src/pikku-request.test.ts +35 -0
  72. package/src/pikku-request.ts +1 -1
  73. package/src/pikku-state.ts +2 -3
  74. package/src/run-tests-script.test.ts +18 -0
  75. package/src/services/ai-agent-runner-service.ts +1 -0
  76. package/src/services/in-memory-queue-service.ts +1 -1
  77. package/src/services/in-memory-session-store.ts +1 -2
  78. package/src/services/in-memory-workflow-service.test.ts +33 -11
  79. package/src/services/in-memory-workflow-service.ts +49 -13
  80. package/src/services/local-content.test.ts +54 -0
  81. package/src/services/local-content.ts +12 -1
  82. package/src/services/typed-credential-service.ts +3 -3
  83. package/src/services/typed-secret-service.ts +3 -3
  84. package/src/services/typed-variables-service.ts +3 -3
  85. package/src/services/user-session-service.ts +4 -4
  86. package/src/testing/service-tests.ts +30 -0
  87. package/src/types/core.types.ts +6 -2
  88. package/src/types/state.types.ts +2 -13
  89. package/src/wirings/ai-agent/ai-agent-memory.test.ts +324 -0
  90. package/src/wirings/ai-agent/ai-agent-memory.ts +187 -36
  91. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +12 -90
  92. package/src/wirings/ai-agent/ai-agent-model-config.ts +14 -38
  93. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +292 -0
  94. package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -1
  95. package/src/wirings/ai-agent/ai-agent-registry.test.ts +230 -3
  96. package/src/wirings/ai-agent/ai-agent-runner.test.ts +625 -6
  97. package/src/wirings/ai-agent/ai-agent-runner.ts +65 -50
  98. package/src/wirings/ai-agent/ai-agent-stream.test.ts +544 -5
  99. package/src/wirings/ai-agent/ai-agent-stream.ts +71 -69
  100. package/src/wirings/ai-agent/ai-agent.types.ts +24 -0
  101. package/src/wirings/channel/channel-handler.test.ts +272 -0
  102. package/src/wirings/channel/channel-handler.ts +1 -1
  103. package/src/wirings/channel/channel-middleware-runner.test.ts +163 -0
  104. package/src/wirings/channel/channel-store.ts +19 -0
  105. package/src/wirings/channel/channel.types.ts +4 -0
  106. package/src/wirings/channel/local/local-channel-runner.test.ts +63 -0
  107. package/src/wirings/channel/local/local-channel-runner.ts +41 -5
  108. package/src/wirings/channel/local/local-eventhub-service.ts +3 -3
  109. package/src/wirings/channel/pikku-abstract-channel-handler.ts +9 -2
  110. package/src/wirings/channel/serverless/serverless-channel-runner.ts +23 -5
  111. package/src/wirings/cli/channel/cli-raw-channel-runner.ts +7 -2
  112. package/src/wirings/cli/cli-runner.test.ts +255 -2
  113. package/src/wirings/cli/cli-runner.ts +31 -10
  114. package/src/wirings/cli/cli.types.ts +4 -8
  115. package/src/wirings/cli/command-parser.test.ts +83 -0
  116. package/src/wirings/cli/command-parser.ts +23 -4
  117. package/src/wirings/http/http-runner.test.ts +296 -1
  118. package/src/wirings/http/http-runner.ts +87 -57
  119. package/src/wirings/http/http.types.ts +11 -26
  120. package/src/wirings/http/pikku-fetch-http-request.ts +8 -4
  121. package/src/wirings/http/pikku-fetch-http-response.test.ts +41 -0
  122. package/src/wirings/http/web-request.test.ts +115 -0
  123. package/src/wirings/http/web-request.ts +43 -5
  124. package/src/wirings/mcp/mcp-runner.test.ts +367 -0
  125. package/src/wirings/queue/queue.types.ts +2 -5
  126. package/src/wirings/rpc/rpc-runner.test.ts +511 -0
  127. package/src/wirings/rpc/rpc-runner.ts +15 -3
  128. package/src/wirings/rpc/wire-addon.test.ts +57 -0
  129. package/src/wirings/workflow/graph/graph-node.ts +12 -0
  130. package/src/wirings/workflow/graph/graph-runner.test.ts +98 -0
  131. package/src/wirings/workflow/graph/graph-runner.ts +28 -10
  132. package/src/wirings/workflow/graph/workflow-graph.types.ts +4 -0
  133. package/src/wirings/workflow/index.ts +1 -0
  134. package/src/wirings/workflow/pikku-workflow-service.test.ts +19 -2
  135. package/src/wirings/workflow/pikku-workflow-service.ts +370 -71
  136. package/src/wirings/workflow/workflow-queue-workers.test.ts +131 -0
  137. package/src/wirings/workflow/workflow.types.ts +68 -5
  138. package/tsconfig.tsbuildinfo +1 -1
@@ -1,4 +1,4 @@
1
- import { saveMessages, resolveMemoryServices, loadContextMessages, trimMessages, } from './ai-agent-memory.js';
1
+ import { saveMessages, resolveMemoryServices, loadContextMessages, trimMessages, getWorkingMemoryMiddleware, } from './ai-agent-memory.js';
2
2
  import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, } from './ai-agent-prepare.js';
3
3
  import { checkForApprovals, appendStepMessages } from './ai-agent-stream.js';
4
4
  import { pikkuState, getSingletonServices } from '../../pikku-state.js';
@@ -20,9 +20,19 @@ function stripNulls(obj) {
20
20
  return result;
21
21
  }
22
22
  import { randomUUID } from 'crypto';
23
+ function extractStructuredAssistantOutput(object) {
24
+ if (!object || typeof object !== 'object') {
25
+ return { text: null, uiSpec: null };
26
+ }
27
+ const record = object;
28
+ return {
29
+ text: typeof record.text === 'string' ? record.text : null,
30
+ uiSpec: record.ui ?? null,
31
+ };
32
+ }
23
33
  export async function runAIAgent(agentName, input, params, agentSessionMap) {
24
34
  const sessionMap = agentSessionMap ?? new Map();
25
- const { agent, agentRunner, storage, memoryConfig, threadId, userMessage, runnerParams, maxSteps, missingRpcs, workingMemoryJsonSchema, workingMemorySchemaName, } = await prepareAgentRun(agentName, input, params, sessionMap);
35
+ const { agent, agentRunner, storage, memoryConfig, threadId, userMessage, runnerParams, maxSteps, missingRpcs, workingMemorySchemaName, } = await prepareAgentRun(agentName, input, params, sessionMap);
26
36
  const singletonServices = getSingletonServices();
27
37
  const { aiRunState } = singletonServices;
28
38
  if (!aiRunState) {
@@ -48,7 +58,15 @@ export async function runAIAgent(agentName, input, params, agentSessionMap) {
48
58
  usage: { inputTokens: 0, outputTokens: 0 },
49
59
  };
50
60
  }
51
- const aiMiddlewares = agent.aiMiddleware ?? [];
61
+ const aiMiddlewares = [
62
+ ...getWorkingMemoryMiddleware(memoryConfig, storage, {
63
+ threadId,
64
+ workingMemorySchemaName,
65
+ logger: singletonServices.logger,
66
+ schemaService: singletonServices.schema,
67
+ }),
68
+ ...(agent.aiMiddleware ?? []),
69
+ ];
52
70
  let modifiedMessages = runnerParams.messages;
53
71
  let modifiedInstructions = runnerParams.instructions;
54
72
  for (const mw of aiMiddlewares) {
@@ -152,12 +170,7 @@ export async function runAIAgent(agentName, input, params, agentSessionMap) {
152
170
  args: a.args,
153
171
  });
154
172
  const completedStepsForSave = accumulatedSteps.slice(0, -1);
155
- await saveMessages(storage, threadId, input.resourceId, memoryConfig, userMessage, { text: '', steps: completedStepsForSave }, {
156
- workingMemoryJsonSchema,
157
- workingMemorySchemaName,
158
- logger: singletonServices.logger,
159
- schemaService: singletonServices.schema,
160
- });
173
+ await saveMessages(storage, threadId, input.resourceId, memoryConfig, userMessage, { text: '', steps: completedStepsForSave });
161
174
  if (storage) {
162
175
  await storage.saveMessages(threadId, [
163
176
  {
@@ -197,19 +210,14 @@ export async function runAIAgent(agentName, input, params, agentSessionMap) {
197
210
  }
198
211
  appendStepMessages(runnerParams, stepResult);
199
212
  }
200
- const finalText = lastStepResult?.text ?? '';
201
213
  const finalObject = lastStepResult?.object;
214
+ const structuredOutput = extractStructuredAssistantOutput(finalObject);
215
+ const finalText = structuredOutput.text ?? lastStepResult?.text ?? '';
202
216
  const result = {
203
217
  text: finalText,
204
218
  steps: accumulatedSteps,
205
219
  };
206
- const responseText = await saveMessages(storage, threadId, input.resourceId, memoryConfig, userMessage, result, {
207
- workingMemoryJsonSchema,
208
- workingMemorySchemaName,
209
- logger: singletonServices.logger,
210
- schemaService: singletonServices.schema,
211
- });
212
- let outputText = responseText;
220
+ let outputText = finalText;
213
221
  let outputMessages = runnerParams.messages;
214
222
  for (let i = aiMiddlewares.length - 1; i >= 0; i--) {
215
223
  const mw = aiMiddlewares[i];
@@ -223,6 +231,11 @@ export async function runAIAgent(agentName, input, params, agentSessionMap) {
223
231
  outputMessages = modResult.messages;
224
232
  }
225
233
  }
234
+ await saveMessages(storage, threadId, input.resourceId, memoryConfig, userMessage, {
235
+ ...result,
236
+ text: outputText,
237
+ uiSpec: structuredOutput.uiSpec,
238
+ });
226
239
  await aiRunState.updateRun(runId, {
227
240
  status: 'completed',
228
241
  usage: { ...totalUsage, model: agent.model },
@@ -369,7 +382,15 @@ async function continueAfterToolResultSync(run, agent, packageName, resolvedName
369
382
  const allMessages = [...contextMessages, ...messages];
370
383
  const trimmedMessages = trimMessages(allMessages);
371
384
  const instructions = await buildInstructions(resolvedName, packageName);
372
- const aiMiddlewares = agent.aiMiddleware ?? [];
385
+ const aiMiddlewares = [
386
+ ...getWorkingMemoryMiddleware(memoryConfig, storage, {
387
+ threadId: run.threadId,
388
+ workingMemorySchemaName,
389
+ logger: singletonServices.logger,
390
+ schemaService: singletonServices.schema,
391
+ }),
392
+ ...(agent.aiMiddleware ?? []),
393
+ ];
373
394
  let modifiedMessages = trimmedMessages;
374
395
  let modifiedInstructions = instructions;
375
396
  for (const mw of aiMiddlewares) {
@@ -463,12 +484,7 @@ async function continueAfterToolResultSync(run, agent, packageName, resolvedName
463
484
  });
464
485
  const completedSteps = accumulatedSteps.slice(0, -1);
465
486
  if (completedSteps.length > 0) {
466
- await saveMessages(storage, run.threadId, run.resourceId, memoryConfig, null, { text: '', steps: completedSteps }, {
467
- workingMemoryJsonSchema,
468
- workingMemorySchemaName,
469
- logger: singletonServices.logger,
470
- schemaService: singletonServices.schema,
471
- });
487
+ await saveMessages(storage, run.threadId, run.resourceId, memoryConfig, null, { text: '', steps: completedSteps });
472
488
  }
473
489
  if (storage) {
474
490
  await storage.saveMessages(run.threadId, [
@@ -515,13 +531,7 @@ async function continueAfterToolResultSync(run, agent, packageName, resolvedName
515
531
  text: finalText,
516
532
  steps: accumulatedSteps,
517
533
  };
518
- const responseText = await saveMessages(storage, run.threadId, run.resourceId, memoryConfig, null, result, {
519
- workingMemoryJsonSchema,
520
- workingMemorySchemaName,
521
- logger: singletonServices.logger,
522
- schemaService: singletonServices.schema,
523
- });
524
- let outputText = responseText;
534
+ let outputText = finalText;
525
535
  let outputMessages = runnerParams.messages;
526
536
  for (let i = aiMiddlewares.length - 1; i >= 0; i--) {
527
537
  const mw = aiMiddlewares[i];
@@ -535,6 +545,10 @@ async function continueAfterToolResultSync(run, agent, packageName, resolvedName
535
545
  outputMessages = modResult.messages;
536
546
  }
537
547
  }
548
+ await saveMessages(storage, run.threadId, run.resourceId, memoryConfig, null, {
549
+ ...result,
550
+ text: outputText,
551
+ });
538
552
  await aiRunState.updateRun(run.runId, {
539
553
  status: 'completed',
540
554
  usage: { ...totalUsage, model: agent.model },
@@ -2,12 +2,13 @@ import { pikkuState, getSingletonServices } from '../../pikku-state.js';
2
2
  import { AIProviderNotConfiguredError } from '../../errors/errors.js';
3
3
  import { combineChannelMiddleware, wrapChannelWithMiddleware, } from '../channel/channel-middleware-runner.js';
4
4
  import { randomUUID } from 'crypto';
5
- import { parseWorkingMemory, deepMergeWorkingMemory, resolveMemoryServices, loadContextMessages, trimMessages, } from './ai-agent-memory.js';
5
+ import { resolveMemoryServices, loadContextMessages, trimMessages, getWorkingMemoryMiddleware, } from './ai-agent-memory.js';
6
6
  import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, createScopedChannel, ToolApprovalRequired, ToolCredentialRequired, } from './ai-agent-prepare.js';
7
7
  import { resolveModelConfig } from './ai-agent-model-config.js';
8
8
  function createPersistingChannel(parent, storage, threadId) {
9
9
  let fullText = '';
10
10
  let stepText = '';
11
+ let stepGenerativeUI = null;
11
12
  let stepToolCalls = [];
12
13
  let stepToolResults = [];
13
14
  const totalUsage = {
@@ -18,16 +19,23 @@ function createPersistingChannel(parent, storage, threadId) {
18
19
  if (!storage)
19
20
  return;
20
21
  const text = stepText;
22
+ const generativeUI = stepGenerativeUI;
21
23
  const calls = stepToolCalls;
22
24
  const results = stepToolResults;
23
25
  stepText = '';
26
+ stepGenerativeUI = null;
24
27
  stepToolCalls = [];
25
28
  stepToolResults = [];
26
- if (text || calls.length > 0) {
29
+ if (text || generativeUI != null || calls.length > 0) {
27
30
  const assistantMsg = {
28
31
  id: randomUUID(),
29
32
  role: 'assistant',
30
- content: text || undefined,
33
+ content: generativeUI != null
34
+ ? [
35
+ ...(text ? [{ type: 'text', text }] : []),
36
+ { type: 'generative-ui', spec: generativeUI },
37
+ ]
38
+ : text || undefined,
31
39
  toolCalls: calls.length > 0 ? calls : undefined,
32
40
  createdAt: new Date(),
33
41
  };
@@ -81,6 +89,9 @@ function createPersistingChannel(parent, storage, threadId) {
81
89
  : JSON.stringify(event.result),
82
90
  });
83
91
  break;
92
+ case 'generative-ui':
93
+ stepGenerativeUI = event.spec;
94
+ break;
84
95
  case 'usage':
85
96
  totalUsage.inputTokens += event.tokens.input;
86
97
  totalUsage.outputTokens += event.tokens.output;
@@ -95,10 +106,13 @@ function createPersistingChannel(parent, storage, threadId) {
95
106
  }
96
107
  parent.send(event);
97
108
  },
109
+ setState: (s) => parent.setState(s),
110
+ getState: () => parent.getState(),
111
+ clearState: () => parent.clearState(),
98
112
  };
99
113
  return channel;
100
114
  }
101
- async function postStreamCleanup(persistingChannel, aiMiddlewares, singletonServices, storage, memoryConfig, threadId, workingMemorySchemaName, messages, aiRunState, runId) {
115
+ async function postStreamCleanup(persistingChannel, aiMiddlewares, singletonServices, messages, aiRunState, runId) {
102
116
  const usage = persistingChannel.totalUsage;
103
117
  let outputText = persistingChannel.fullText;
104
118
  let outputMessages = messages;
@@ -117,22 +131,6 @@ async function postStreamCleanup(persistingChannel, aiMiddlewares, singletonServ
117
131
  outputMessages = result.messages;
118
132
  }
119
133
  }
120
- if (storage && memoryConfig?.workingMemory && outputText) {
121
- const parsed = parseWorkingMemory(outputText);
122
- if (parsed) {
123
- const existing = (await storage.getWorkingMemory(threadId, 'thread')) ?? {};
124
- const merged = deepMergeWorkingMemory(existing, parsed);
125
- if (singletonServices.schema && workingMemorySchemaName) {
126
- try {
127
- await singletonServices.schema.validateSchema(workingMemorySchemaName, merged);
128
- }
129
- catch (err) {
130
- singletonServices.logger.warn(`Working memory validation failed: ${err instanceof Error ? err.message : String(err)}`);
131
- }
132
- }
133
- await storage.saveWorkingMemory(threadId, 'thread', merged);
134
- }
135
- }
136
134
  await aiRunState.updateRun(runId, {
137
135
  status: 'completed',
138
136
  ...(usage.model
@@ -147,7 +145,7 @@ async function postStreamCleanup(persistingChannel, aiMiddlewares, singletonServ
147
145
  });
148
146
  }
149
147
  async function runStreamStepLoop(params) {
150
- const { agent, runnerParams, maxSteps, agentRunner, persistingChannel, channel, runId, aiMiddlewares, } = params;
148
+ const { agent, runnerParams, maxSteps, agentRunner, streamChannel, channel, runId, aiMiddlewares, } = params;
151
149
  const singletonServices = getSingletonServices();
152
150
  for (let step = 0; step < maxSteps; step++) {
153
151
  if (agent.prepareStep) {
@@ -166,7 +164,7 @@ async function runStreamStepLoop(params) {
166
164
  break;
167
165
  }
168
166
  channel.send({ type: 'step-start', stepNumber: step });
169
- const stepResult = await agentRunner.stream(runnerParams, persistingChannel);
167
+ const stepResult = await agentRunner.stream(runnerParams, streamChannel);
170
168
  for (const mw of aiMiddlewares) {
171
169
  if (mw.afterStep) {
172
170
  await mw.afterStep(singletonServices, {
@@ -246,10 +244,21 @@ export function checkForCredentialRequests(stepResult, runId) {
246
244
  return requests;
247
245
  }
248
246
  export function appendStepMessages(runnerParams, stepResult) {
247
+ const structuredOutput = stepResult.object && typeof stepResult.object === 'object'
248
+ ? stepResult.object
249
+ : null;
250
+ const assistantContent = structuredOutput?.ui != null
251
+ ? [
252
+ ...(stepResult.text
253
+ ? [{ type: 'text', text: stepResult.text }]
254
+ : []),
255
+ { type: 'generative-ui', spec: structuredOutput.ui },
256
+ ]
257
+ : stepResult.text || undefined;
249
258
  const assistantMsg = {
250
259
  id: randomUUID(),
251
260
  role: 'assistant',
252
- content: stepResult.text || undefined,
261
+ content: assistantContent,
253
262
  toolCalls: stepResult.toolCalls.length > 0
254
263
  ? stepResult.toolCalls.map((tc) => ({
255
264
  id: tc.toolCallId,
@@ -257,6 +266,9 @@ export function appendStepMessages(runnerParams, stepResult) {
257
266
  args: tc.args,
258
267
  }))
259
268
  : undefined,
269
+ ...(stepResult.reasoningContent
270
+ ? { reasoningContent: stepResult.reasoningContent }
271
+ : {}),
260
272
  createdAt: new Date(),
261
273
  };
262
274
  runnerParams.messages.push(assistantMsg);
@@ -363,7 +375,15 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
363
375
  channel.send({ type: 'done' });
364
376
  return '';
365
377
  }
366
- const aiMiddlewares = agent.aiMiddleware ?? [];
378
+ const aiMiddlewares = [
379
+ ...getWorkingMemoryMiddleware(memoryConfig, storage, {
380
+ threadId,
381
+ workingMemorySchemaName,
382
+ logger: singletonServices.logger,
383
+ schemaService: singletonServices.schema,
384
+ }),
385
+ ...(agent.aiMiddleware ?? []),
386
+ ];
367
387
  let modifiedMessages = runnerParams.messages;
368
388
  let modifiedInstructions = runnerParams.instructions;
369
389
  for (const mw of aiMiddlewares) {
@@ -422,9 +442,10 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
422
442
  ...streamMiddleware,
423
443
  ],
424
444
  });
445
+ const persistingChannel = createPersistingChannel(channel, storage, threadId);
425
446
  const wrappedChannel = allChannelMiddleware.length > 0
426
- ? wrapChannelWithMiddleware({ channel }, singletonServices, allChannelMiddleware).channel
427
- : channel;
447
+ ? wrapChannelWithMiddleware({ channel: persistingChannel }, singletonServices, allChannelMiddleware).channel
448
+ : persistingChannel;
428
449
  // In delegate mode (default), suppress parent's text from reaching the client
429
450
  // AFTER a sub-agent has been called. If the parent responds directly (no delegation),
430
451
  // its text goes through normally. Sub-agent text bypasses this path entirely
@@ -446,13 +467,13 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
446
467
  delegateState,
447
468
  }
448
469
  : wrappedChannel;
449
- const persistingChannel = createPersistingChannel(outputChannel, storage, threadId);
450
470
  try {
451
471
  const loopResult = await runStreamStepLoop({
452
472
  agent,
453
473
  runnerParams,
454
474
  maxSteps,
455
475
  agentRunner,
476
+ streamChannel: outputChannel,
456
477
  persistingChannel,
457
478
  channel,
458
479
  runId,
@@ -466,7 +487,7 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
466
487
  await handleCredentialRequests(loopResult.credentialRequests, runId, channel, aiRunState, persistingChannel);
467
488
  return persistingChannel.fullText;
468
489
  }
469
- await postStreamCleanup(persistingChannel, aiMiddlewares, singletonServices, storage, memoryConfig, threadId, workingMemorySchemaName, runnerParams.messages, aiRunState, runId);
490
+ await postStreamCleanup(persistingChannel, aiMiddlewares, singletonServices, runnerParams.messages, aiRunState, runId);
470
491
  channel.send({ type: 'done' });
471
492
  channel.close();
472
493
  return persistingChannel.fullText;
@@ -683,7 +704,15 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
683
704
  const allMessages = [...contextMessages, ...messages];
684
705
  const trimmedMessages = trimMessages(allMessages);
685
706
  const instructions = await buildInstructions(resolvedName, packageName);
686
- const aiMiddlewares = agent.aiMiddleware ?? [];
707
+ const aiMiddlewares = [
708
+ ...getWorkingMemoryMiddleware(memoryConfig, storage, {
709
+ threadId: run.threadId,
710
+ workingMemorySchemaName,
711
+ logger: singletonServices.logger,
712
+ schemaService: singletonServices.schema,
713
+ }),
714
+ ...(agent.aiMiddleware ?? []),
715
+ ];
687
716
  let modifiedMessages = trimmedMessages;
688
717
  let modifiedInstructions = instructions;
689
718
  for (const mw of aiMiddlewares) {
@@ -726,14 +755,13 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
726
755
  ...streamMiddleware,
727
756
  ],
728
757
  });
758
+ const persistingChannel = createPersistingChannel(channel, storage, run.threadId);
729
759
  const wrappedChannel = allChannelMiddleware.length > 0
730
- ? wrapChannelWithMiddleware({ channel }, singletonServices, allChannelMiddleware).channel
731
- : channel;
732
- const persistingChannel = createPersistingChannel(wrappedChannel, storage, run.threadId);
760
+ ? wrapChannelWithMiddleware({ channel: persistingChannel }, singletonServices, allChannelMiddleware).channel
761
+ : persistingChannel;
733
762
  const streamContext = { channel, options };
734
763
  const resumeTools = (await buildToolDefs(params, new Map(), run.resourceId, resolvedName, packageName, streamContext, aiMiddlewares)).tools;
735
764
  const resolved = resolveModelConfig(resolvedName, agent);
736
- console.log('[DEBUG resume] resolvedName:', resolvedName, 'agent.model:', agent.model, 'resolved.model:', resolved.model);
737
765
  const maxSteps = resolved.maxSteps ?? 10;
738
766
  const runnerParams = {
739
767
  model: resolved.model,
@@ -753,6 +781,7 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
753
781
  runnerParams,
754
782
  maxSteps,
755
783
  agentRunner,
784
+ streamChannel: wrappedChannel,
756
785
  persistingChannel,
757
786
  channel,
758
787
  runId: run.runId,
@@ -766,7 +795,7 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
766
795
  await handleCredentialRequests(loopResult.credentialRequests, run.runId, channel, aiRunState, persistingChannel);
767
796
  return;
768
797
  }
769
- await postStreamCleanup(persistingChannel, aiMiddlewares, singletonServices, storage, memoryConfig, run.threadId, workingMemorySchemaName, runnerParams.messages, aiRunState, run.runId);
798
+ await postStreamCleanup(persistingChannel, aiMiddlewares, singletonServices, runnerParams.messages, aiRunState, run.runId);
770
799
  channel.send({ type: 'done' });
771
800
  channel.close();
772
801
  }
@@ -24,6 +24,13 @@ export type AIContentPart = {
24
24
  url?: string;
25
25
  mediaType: string;
26
26
  filename?: string;
27
+ } | {
28
+ type: 'data';
29
+ name: string;
30
+ data: unknown;
31
+ } | {
32
+ type: 'generative-ui';
33
+ spec: unknown;
27
34
  };
28
35
  export interface AIToolCall {
29
36
  id: string;
@@ -41,6 +48,7 @@ export interface AIMessage {
41
48
  content?: string | AIContentPart[];
42
49
  toolCalls?: AIToolCall[];
43
50
  toolResults?: AIToolResult[];
51
+ reasoningContent?: string;
44
52
  createdAt: Date;
45
53
  }
46
54
  export interface AIAgentStep {
@@ -298,6 +306,17 @@ export type AIStreamEvent = {
298
306
  graph: any;
299
307
  agent?: string;
300
308
  session?: string;
309
+ } | {
310
+ type: 'data';
311
+ name: string;
312
+ data: unknown;
313
+ agent?: string;
314
+ session?: string;
315
+ } | {
316
+ type: 'generative-ui';
317
+ spec: unknown;
318
+ agent?: string;
319
+ session?: string;
301
320
  } | {
302
321
  type: 'suspended';
303
322
  reason: 'rpc-missing';
@@ -364,6 +383,7 @@ export interface AgentRunRow {
364
383
  export interface AgentRunService {
365
384
  listThreads(options?: {
366
385
  agentName?: string;
386
+ resourceId?: string;
367
387
  limit?: number;
368
388
  offset?: number;
369
389
  }): Promise<AIThread[]>;
@@ -43,7 +43,7 @@ export const processMessageHandlers = (services, channelConfig, channelHandler,
43
43
  const routeMessage = routingProperty
44
44
  ? `route '${routingProperty}:${routerValue}'`
45
45
  : 'the default message route';
46
- logger.error(`Channel ${channelConfig.name} with id ${channelHandler.getChannel().channelId} requires a session for ${routeMessage}`);
46
+ logger.error(`Channel ${channelConfig.name} with id ${channelHandler.getChannel().channelId} requires a session for ${routeMessage}. No session is attached to this websocket connection. Ensure auth middleware establishes a session during websocket upgrade, and configure sessionStore when the runtime needs persisted sessions.`);
47
47
  // TODO: Send error message back breaks typescript, but should be implemented somehow
48
48
  channelHandler.getChannel().send(`Unauthorized for ${routeMessage}`);
49
49
  return;
@@ -13,4 +13,17 @@ export declare abstract class ChannelStore<ChannelType = unknown, OpeningData =
13
13
  }> | (TypedChannel & {
14
14
  pikkuUserId?: string;
15
15
  });
16
+ /**
17
+ * Persist per-socket scratch state scoped to a single channel (keyed by
18
+ * channelId). This is intentionally separate from `SessionStore` (which is
19
+ * keyed by `pikkuUserId` and holds the user session — shared across HTTP
20
+ * and channel transports).
21
+ *
22
+ * Use this for ephemeral, channel-local data: a per-socket subscription
23
+ * filter, a step in a connection-bound state machine, the last command sent
24
+ * on this socket, etc. It is cleared when the channel is removed.
25
+ */
26
+ abstract setState(channelId: string, state: unknown): Promise<void> | void;
27
+ abstract getState(channelId: string): Promise<unknown | undefined> | unknown | undefined;
28
+ abstract clearState(channelId: string): Promise<void> | void;
16
29
  }
@@ -80,6 +80,9 @@ export interface PikkuChannel<OpeningData, out Out> {
80
80
  sendBinary(data: BinaryData): Promise<void> | void;
81
81
  close(): Promise<void> | void;
82
82
  state: 'initial' | 'open' | 'closed';
83
+ setState<T>(state: T): Promise<void> | void;
84
+ getState<T>(): Promise<T | undefined> | T | undefined;
85
+ clearState(): Promise<void> | void;
83
86
  }
84
87
  export interface PikkuChannelHandler<OpeningData = unknown, Out = unknown> {
85
88
  send(message: Out, isBinary?: boolean): Promise<void> | void;
@@ -7,8 +7,26 @@ import { isProduction } from '../../../env.js';
7
7
  import { getErrorResponse } from '../../../errors/error-handler.js';
8
8
  import { handleHTTPError } from '../../../handle-error.js';
9
9
  import { PikkuSessionService, createMiddlewareSessionWireProps, } from '../../../services/user-session-service.js';
10
+ import { combineMiddleware, runMiddleware } from '../../../middleware-runner.js';
10
11
  import { runChannelLifecycleWithMiddleware } from '../channel-common.js';
11
- import { getSingletonServices, getCreateWireServices, } from '../../../pikku-state.js';
12
+ import { getSingletonServices, getCreateWireServices, pikkuState, } from '../../../pikku-state.js';
13
+ const runUpgradeMiddleware = async (singletonServices, request, response, userSession) => {
14
+ const http = createHTTPWire(request, response);
15
+ const httpGroups = pikkuState(null, 'middleware', 'httpGroup');
16
+ const wireInheritedMiddleware = httpGroups['*']
17
+ ? [{ type: 'http', route: '*' }]
18
+ : undefined;
19
+ const middleware = combineMiddleware('channel', `upgrade:${request.path()}`, {
20
+ wireInheritedMiddleware,
21
+ });
22
+ if (middleware.length > 0) {
23
+ await runMiddleware(singletonServices, {
24
+ http,
25
+ ...createMiddlewareSessionWireProps(userSession),
26
+ }, middleware);
27
+ }
28
+ return http;
29
+ };
12
30
  export const runLocalChannel = async ({ channelId, request, response, route, skipUserSession = false, respondWith404 = true, coerceDataFromSchema = true, logWarningsForStatusCodes = [], bubbleErrors = false, }) => {
13
31
  const singletonServices = getSingletonServices();
14
32
  const createWireServices = getCreateWireServices();
@@ -17,12 +35,12 @@ export const runLocalChannel = async ({ channelId, request, response, route, ski
17
35
  let channelHandler;
18
36
  const userSession = new PikkuSessionService(singletonServices.sessionStore);
19
37
  let http;
20
- if (request) {
21
- http = createHTTPWire(request, response);
22
- route = http?.request?.path();
23
- }
24
38
  let openingData, channelConfig, meta;
25
39
  try {
40
+ if (request) {
41
+ http = await runUpgradeMiddleware(singletonServices, request, response, userSession);
42
+ route = request.path();
43
+ }
26
44
  ;
27
45
  ({ openingData, channelConfig, meta } = await openChannel({
28
46
  channelId,
@@ -10,6 +10,7 @@ export class PikkuAbstractChannelHandler {
10
10
  }
11
11
  getChannel() {
12
12
  if (!this.channel) {
13
+ let channelState;
13
14
  this.channel = {
14
15
  channelId: this.channelId,
15
16
  openingData: this.openingData,
@@ -17,6 +18,13 @@ export class PikkuAbstractChannelHandler {
17
18
  sendBinary: this.sendBinary.bind(this),
18
19
  close: this.close.bind(this),
19
20
  state: 'initial',
21
+ setState: (s) => {
22
+ channelState = s;
23
+ },
24
+ getState: () => channelState,
25
+ clearState: () => {
26
+ channelState = undefined;
27
+ },
20
28
  };
21
29
  }
22
30
  return this.channel;
@@ -54,6 +54,9 @@ export const runChannelConnect = async ({ channelId, channelObject, request, res
54
54
  channelHandlerFactory,
55
55
  channelName: channelConfig.name,
56
56
  });
57
+ channel.setState = (state) => channelStore.setState(channelId, state);
58
+ channel.getState = () => channelStore.getState(channelId);
59
+ channel.clearState = () => channelStore.clearState(channelId);
57
60
  const wire = {
58
61
  channel,
59
62
  ...createMiddlewareSessionWireProps(userSession),
@@ -116,6 +119,9 @@ export const runChannelDisconnect = async ({ channelId, channelStore, channelHan
116
119
  openingData,
117
120
  channelName,
118
121
  });
122
+ channel.setState = (state) => channelStore.setState(channelId, state);
123
+ channel.getState = () => channelStore.getState(channelId);
124
+ channel.clearState = () => channelStore.clearState(channelId);
119
125
  const userSession = new PikkuSessionService(singletonServices.sessionStore);
120
126
  if (pikkuUserId) {
121
127
  userSession.setPikkuUserId(pikkuUserId);
@@ -167,6 +173,9 @@ export const runChannelMessage = async ({ channelId, channelStore, channelHandle
167
173
  openingData,
168
174
  channelName,
169
175
  });
176
+ channel.setState = (state) => channelStore.setState(channelId, state);
177
+ channel.getState = () => channelStore.getState(channelId);
178
+ channel.clearState = () => channelStore.clearState(channelId);
170
179
  const userSession = new PikkuSessionService(singletonServices.sessionStore);
171
180
  if (pikkuUserId) {
172
181
  userSession.setPikkuUserId(pikkuUserId);
@@ -208,6 +208,7 @@ export async function runCLICommand({ program, commandPath, data, singletonServi
208
208
  // Get the renderer
209
209
  const renderer = programData?.renderers[commandId] || programData?.defaultRenderer;
210
210
  // Create a CLI channel for progressive output
211
+ let cliState;
211
212
  const channel = {
212
213
  channelId: `cli:${program}:${commandId}`,
213
214
  openingData: pluckedData,
@@ -225,6 +226,13 @@ export async function runCLICommand({ program, commandPath, data, singletonServi
225
226
  }
226
227
  },
227
228
  state: 'open',
229
+ setState: (s) => {
230
+ cliState = s;
231
+ },
232
+ getState: () => cliState,
233
+ clearState: () => {
234
+ cliState = undefined;
235
+ },
228
236
  };
229
237
  const userSession = new PikkuSessionService(singletonServices.sessionStore);
230
238
  const wire = {
@@ -252,10 +260,20 @@ export async function runCLICommand({ program, commandPath, data, singletonServi
252
260
  sessionService: userSession,
253
261
  packageName: currentCommand.packageName,
254
262
  });
255
- // Apply renderer one final time with the final output (if renderer exists)
256
- // Skip if result is undefined (void functions handle their own output)
257
- if (renderer && result !== undefined) {
258
- await Promise.resolve(renderer(singletonServices, result, userSession.get()));
263
+ // Render the final result. `--json` / `--output json` routes the result
264
+ // through the global JSON renderer, but only for commands that opted into
265
+ // result-rendering by declaring a `render` — inline-printing commands (no
266
+ // per-command renderer) keep emitting their own output untouched.
267
+ // Skip if result is undefined (void functions handle their own output).
268
+ if (result !== undefined) {
269
+ const commandRenderer = programData?.renderers[commandId];
270
+ const jsonMode = data.json === true || data.output === 'json';
271
+ const finalRenderer = jsonMode && commandRenderer !== undefined
272
+ ? defaultJSONRenderer
273
+ : (commandRenderer ?? programData?.defaultRenderer);
274
+ if (finalRenderer !== undefined) {
275
+ await Promise.resolve(finalRenderer(singletonServices, result, userSession.get()));
276
+ }
259
277
  }
260
278
  return result;
261
279
  }
@@ -305,7 +323,8 @@ export async function executeCLI({ programName, args, createConfig, createSingle
305
323
  if (parsed.errors.length > 0) {
306
324
  // Check if any error is about an unknown command
307
325
  const hasUnknownCommand = parsed.errors.some((error) => error.startsWith('Unknown command:') ||
308
- error.startsWith('Command not found:'));
326
+ error.startsWith('Command not found:') ||
327
+ error.startsWith('Missing subcommand:'));
309
328
  if (hasUnknownCommand) {
310
329
  // Show help instead of error for unknown commands
311
330
  const helpText = generateCommandHelp(programName, allCLIMeta, parsed.commandPath);