@pikku/core 0.12.20 → 0.12.22

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 (141) hide show
  1. package/CHANGELOG.md +17 -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/handle-error.js +3 -1
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.js +2 -2
  9. package/dist/middleware/index.d.ts +2 -2
  10. package/dist/middleware/index.js +2 -2
  11. package/dist/middleware/telemetry.d.ts +2 -2
  12. package/dist/middleware/telemetry.js +2 -2
  13. package/dist/middleware-runner.d.ts +15 -27
  14. package/dist/middleware-runner.js +25 -30
  15. package/dist/permissions.d.ts +15 -22
  16. package/dist/permissions.js +30 -25
  17. package/dist/pikku-request.js +1 -1
  18. package/dist/pikku-state.js +2 -3
  19. package/dist/services/ai-agent-runner-service.d.ts +1 -0
  20. package/dist/services/in-memory-queue-service.js +1 -1
  21. package/dist/services/in-memory-workflow-service.d.ts +15 -13
  22. package/dist/services/in-memory-workflow-service.js +31 -13
  23. package/dist/services/local-content.js +8 -1
  24. package/dist/services/user-session-service.d.ts +1 -1
  25. package/dist/testing/service-tests.js +24 -0
  26. package/dist/types/core.types.d.ts +4 -0
  27. package/dist/types/state.types.d.ts +2 -18
  28. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +24 -5
  29. package/dist/wirings/ai-agent/ai-agent-memory.js +128 -23
  30. package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +10 -1
  31. package/dist/wirings/ai-agent/ai-agent-model-config.js +15 -35
  32. package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -1
  33. package/dist/wirings/ai-agent/ai-agent-runner.js +45 -31
  34. package/dist/wirings/ai-agent/ai-agent-stream.js +63 -34
  35. package/dist/wirings/ai-agent/ai-agent.types.d.ts +20 -0
  36. package/dist/wirings/channel/channel-handler.js +1 -1
  37. package/dist/wirings/channel/channel-store.d.ts +13 -0
  38. package/dist/wirings/channel/channel.types.d.ts +3 -0
  39. package/dist/wirings/channel/local/local-channel-runner.js +23 -5
  40. package/dist/wirings/channel/pikku-abstract-channel-handler.js +8 -0
  41. package/dist/wirings/channel/serverless/serverless-channel-runner.js +9 -0
  42. package/dist/wirings/cli/cli-runner.js +24 -5
  43. package/dist/wirings/cli/command-parser.js +19 -4
  44. package/dist/wirings/http/http-runner.js +72 -36
  45. package/dist/wirings/http/http.types.d.ts +1 -0
  46. package/dist/wirings/http/pikku-fetch-http-request.js +3 -1
  47. package/dist/wirings/http/web-request.js +32 -0
  48. package/dist/wirings/rpc/rpc-runner.js +13 -3
  49. package/dist/wirings/workflow/graph/graph-node.d.ts +8 -0
  50. package/dist/wirings/workflow/graph/graph-node.js +4 -0
  51. package/dist/wirings/workflow/graph/graph-runner.js +12 -10
  52. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +4 -0
  53. package/dist/wirings/workflow/index.d.ts +1 -1
  54. package/dist/wirings/workflow/pikku-workflow-service.d.ts +81 -16
  55. package/dist/wirings/workflow/pikku-workflow-service.js +266 -45
  56. package/dist/wirings/workflow/workflow.types.d.ts +34 -0
  57. package/package.json +1 -1
  58. package/run-tests.sh +4 -1
  59. package/src/dev/hot-reload.test.ts +62 -11
  60. package/src/dev/hot-reload.ts +28 -7
  61. package/src/errors/error-handler.ts +8 -2
  62. package/src/errors/error.test.ts +2 -0
  63. package/src/function/function-runner.test.ts +500 -10
  64. package/src/function/functions.types.ts +3 -2
  65. package/src/handle-error.test.ts +1 -1
  66. package/src/handle-error.ts +4 -1
  67. package/src/index.ts +12 -2
  68. package/src/middleware/index.ts +11 -2
  69. package/src/middleware/telemetry.ts +2 -2
  70. package/src/middleware-runner.test.ts +16 -16
  71. package/src/middleware-runner.ts +42 -30
  72. package/src/permissions.test.ts +27 -24
  73. package/src/permissions.ts +41 -25
  74. package/src/pikku-request.test.ts +35 -0
  75. package/src/pikku-request.ts +1 -1
  76. package/src/pikku-state.ts +2 -3
  77. package/src/run-tests-script.test.ts +18 -0
  78. package/src/services/ai-agent-runner-service.ts +1 -0
  79. package/src/services/in-memory-queue-service.ts +1 -1
  80. package/src/services/in-memory-session-store.ts +1 -2
  81. package/src/services/in-memory-workflow-service.test.ts +33 -11
  82. package/src/services/in-memory-workflow-service.ts +49 -13
  83. package/src/services/local-content.test.ts +54 -0
  84. package/src/services/local-content.ts +12 -1
  85. package/src/services/typed-credential-service.ts +3 -3
  86. package/src/services/typed-secret-service.ts +3 -3
  87. package/src/services/typed-variables-service.ts +3 -3
  88. package/src/services/user-session-service.ts +4 -4
  89. package/src/testing/service-tests.ts +30 -0
  90. package/src/types/core.types.ts +6 -2
  91. package/src/types/state.types.ts +2 -13
  92. package/src/wirings/ai-agent/ai-agent-memory.test.ts +324 -0
  93. package/src/wirings/ai-agent/ai-agent-memory.ts +187 -36
  94. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +12 -90
  95. package/src/wirings/ai-agent/ai-agent-model-config.ts +14 -38
  96. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +292 -0
  97. package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -1
  98. package/src/wirings/ai-agent/ai-agent-registry.test.ts +230 -3
  99. package/src/wirings/ai-agent/ai-agent-runner.test.ts +625 -6
  100. package/src/wirings/ai-agent/ai-agent-runner.ts +65 -50
  101. package/src/wirings/ai-agent/ai-agent-stream.test.ts +544 -5
  102. package/src/wirings/ai-agent/ai-agent-stream.ts +71 -69
  103. package/src/wirings/ai-agent/ai-agent.types.ts +24 -0
  104. package/src/wirings/channel/channel-handler.test.ts +272 -0
  105. package/src/wirings/channel/channel-handler.ts +1 -1
  106. package/src/wirings/channel/channel-middleware-runner.test.ts +163 -0
  107. package/src/wirings/channel/channel-store.ts +19 -0
  108. package/src/wirings/channel/channel.types.ts +4 -0
  109. package/src/wirings/channel/local/local-channel-runner.test.ts +63 -0
  110. package/src/wirings/channel/local/local-channel-runner.ts +41 -5
  111. package/src/wirings/channel/local/local-eventhub-service.ts +3 -3
  112. package/src/wirings/channel/pikku-abstract-channel-handler.ts +9 -2
  113. package/src/wirings/channel/serverless/serverless-channel-runner.ts +23 -5
  114. package/src/wirings/cli/channel/cli-raw-channel-runner.ts +7 -2
  115. package/src/wirings/cli/cli-runner.test.ts +255 -2
  116. package/src/wirings/cli/cli-runner.ts +31 -10
  117. package/src/wirings/cli/cli.types.ts +4 -8
  118. package/src/wirings/cli/command-parser.test.ts +83 -0
  119. package/src/wirings/cli/command-parser.ts +23 -4
  120. package/src/wirings/http/http-runner.test.ts +296 -1
  121. package/src/wirings/http/http-runner.ts +87 -57
  122. package/src/wirings/http/http.types.ts +11 -26
  123. package/src/wirings/http/pikku-fetch-http-request.ts +8 -4
  124. package/src/wirings/http/pikku-fetch-http-response.test.ts +41 -0
  125. package/src/wirings/http/web-request.test.ts +115 -0
  126. package/src/wirings/http/web-request.ts +43 -5
  127. package/src/wirings/mcp/mcp-runner.test.ts +367 -0
  128. package/src/wirings/queue/queue.types.ts +2 -5
  129. package/src/wirings/rpc/rpc-runner.test.ts +511 -0
  130. package/src/wirings/rpc/rpc-runner.ts +15 -3
  131. package/src/wirings/rpc/wire-addon.test.ts +57 -0
  132. package/src/wirings/workflow/graph/graph-node.ts +12 -0
  133. package/src/wirings/workflow/graph/graph-runner.test.ts +98 -0
  134. package/src/wirings/workflow/graph/graph-runner.ts +28 -10
  135. package/src/wirings/workflow/graph/workflow-graph.types.ts +4 -0
  136. package/src/wirings/workflow/index.ts +1 -0
  137. package/src/wirings/workflow/pikku-workflow-service.test.ts +19 -2
  138. package/src/wirings/workflow/pikku-workflow-service.ts +370 -71
  139. package/src/wirings/workflow/workflow-queue-workers.test.ts +131 -0
  140. package/src/wirings/workflow/workflow.types.ts +68 -5
  141. package/tsconfig.tsbuildinfo +1 -1
@@ -16,6 +16,7 @@ import { PikkuWorkflowService } from '../wirings/workflow/pikku-workflow-service
16
16
  * ```
17
17
  */
18
18
  export class InMemoryWorkflowService extends PikkuWorkflowService {
19
+ sleepTimers = new Set();
19
20
  runs = new Map();
20
21
  steps = new Map(); // keyed by `${runId}:${stepName}`
21
22
  stepData = new Map(); // keyed by stepId
@@ -23,7 +24,7 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
23
24
  runState = new Map(); // keyed by runId
24
25
  branchKeys = new Map(); // keyed by stepId
25
26
  workflowVersions = new Map(); // keyed by `${name}:${graphHash}`
26
- async createRun(workflowName, input, inline, graphHash, wire, options) {
27
+ async createRunImpl(workflowName, input, inline, graphHash, wire, options) {
27
28
  const runId = randomUUID();
28
29
  const now = new Date();
29
30
  const run = {
@@ -47,13 +48,26 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
47
48
  }
48
49
  return runId;
49
50
  }
51
+ async scheduleSleep(runId, stepId, duration) {
52
+ const timer = setTimeout(async () => {
53
+ this.sleepTimers.delete(timer);
54
+ try {
55
+ await this.executeWorkflowSleepCompleted(runId, stepId);
56
+ }
57
+ catch (error) {
58
+ this.logger?.error(`Failed to resume workflow sleep for runId ${runId}: ${error?.message ?? error}`);
59
+ }
60
+ }, duration);
61
+ this.sleepTimers.add(timer);
62
+ return true;
63
+ }
50
64
  async getRun(id) {
51
65
  return this.runs.get(id) || null;
52
66
  }
53
67
  async getRunHistory(runId) {
54
68
  return this.stepHistory.get(runId) || [];
55
69
  }
56
- async updateRunStatus(id, status, output, error) {
70
+ async updateRunStatusImpl(id, status, output, error) {
57
71
  const run = this.runs.get(id);
58
72
  if (run) {
59
73
  run.status = status;
@@ -64,7 +78,7 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
64
78
  run.error = error;
65
79
  }
66
80
  }
67
- async insertStepState(runId, stepName, rpcName, data, stepOptions) {
81
+ async insertStepStateImpl(runId, stepName, rpcName, data, stepOptions) {
68
82
  const stepId = randomUUID();
69
83
  const now = new Date();
70
84
  const step = {
@@ -94,7 +108,7 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
94
108
  }
95
109
  return step;
96
110
  }
97
- async setStepRunning(stepId) {
111
+ async setStepRunningImpl(stepId) {
98
112
  for (const step of this.steps.values()) {
99
113
  if (step.stepId === stepId) {
100
114
  step.status = 'running';
@@ -104,7 +118,7 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
104
118
  }
105
119
  }
106
120
  }
107
- async setStepScheduled(stepId) {
121
+ async setStepScheduledImpl(stepId) {
108
122
  for (const step of this.steps.values()) {
109
123
  if (step.stepId === stepId) {
110
124
  step.status = 'scheduled';
@@ -114,7 +128,7 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
114
128
  }
115
129
  }
116
130
  }
117
- async setStepResult(stepId, result) {
131
+ async setStepResultImpl(stepId, result) {
118
132
  for (const step of this.steps.values()) {
119
133
  if (step.stepId === stepId) {
120
134
  step.status = 'succeeded';
@@ -125,7 +139,7 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
125
139
  }
126
140
  }
127
141
  }
128
- async setStepError(stepId, error) {
142
+ async setStepErrorImpl(stepId, error) {
129
143
  for (const step of this.steps.values()) {
130
144
  if (step.stepId === stepId) {
131
145
  step.status = 'failed';
@@ -140,7 +154,7 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
140
154
  }
141
155
  }
142
156
  }
143
- async setStepChildRunId(stepId, childRunId) {
157
+ async setStepChildRunIdImpl(stepId, childRunId) {
144
158
  for (const step of this.steps.values()) {
145
159
  if (step.stepId === stepId) {
146
160
  step.childRunId = childRunId;
@@ -149,7 +163,7 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
149
163
  }
150
164
  }
151
165
  }
152
- async createRetryAttempt(failedStepId, status) {
166
+ async createRetryAttemptImpl(failedStepId, status) {
153
167
  // Find the failed step
154
168
  let failedStep;
155
169
  let runId;
@@ -252,6 +266,10 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
252
266
  return fn();
253
267
  }
254
268
  async close() {
269
+ for (const timer of this.sleepTimers) {
270
+ clearTimeout(timer);
271
+ }
272
+ this.sleepTimers.clear();
255
273
  // Clear all in-memory state
256
274
  this.runs.clear();
257
275
  this.steps.clear();
@@ -317,10 +335,10 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
317
335
  this.branchKeys.set(step.stepId, branchKey);
318
336
  }
319
337
  }
320
- async setBranchTaken(stepId, branchKey) {
338
+ async setBranchTakenImpl(stepId, branchKey) {
321
339
  this.branchKeys.set(stepId, branchKey);
322
340
  }
323
- async updateRunState(runId, name, value) {
341
+ async updateRunStateImpl(runId, name, value) {
324
342
  const state = this.runState.get(runId) || {};
325
343
  state[name] = value;
326
344
  this.runState.set(runId, state);
@@ -328,14 +346,14 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
328
346
  async getRunState(runId) {
329
347
  return this.runState.get(runId) || {};
330
348
  }
331
- async upsertWorkflowVersion(name, graphHash, graph, source, status) {
349
+ async upsertWorkflowVersionImpl(name, graphHash, graph, source, status) {
332
350
  this.workflowVersions.set(`${name}:${graphHash}`, {
333
351
  graph,
334
352
  source,
335
353
  status: status ?? 'active',
336
354
  });
337
355
  }
338
- async updateWorkflowVersionStatus(name, graphHash, status) {
356
+ async updateWorkflowVersionStatusImpl(name, graphHash, status) {
339
357
  const key = `${name}:${graphHash}`;
340
358
  const version = this.workflowVersions.get(key);
341
359
  if (version) {
@@ -36,7 +36,14 @@ export class LocalContent {
36
36
  }
37
37
  if (this.jwt) {
38
38
  const expiresInSeconds = Math.max(1, Math.floor((expiresAt - signedAt) / 1000));
39
- const signature = await this.jwt.encode({ value: expiresInSeconds, unit: 'second' }, { signedAt, expiresAt });
39
+ const payload = {
40
+ signedAt,
41
+ expiresAt,
42
+ };
43
+ if (dateGreaterThan) {
44
+ payload.notBefore = dateGreaterThan.getTime();
45
+ }
46
+ const signature = await this.jwt.encode({ value: expiresInSeconds, unit: 'second' }, payload);
40
47
  params.set('signature', signature);
41
48
  }
42
49
  return params.toString();
@@ -13,7 +13,7 @@ export declare class PikkuSessionService<UserSession extends CoreUserSession> im
13
13
  private sessionStore?;
14
14
  sessionChanged: boolean;
15
15
  initial: UserSession | undefined;
16
- private session;
16
+ protected session: UserSession | undefined;
17
17
  private pikkuUserId?;
18
18
  constructor(sessionStore?: SessionStore<UserSession> | undefined);
19
19
  setPikkuUserId(id: string): void;
@@ -49,6 +49,30 @@ export function defineServiceTests(config) {
49
49
  test('removeChannels with empty array is no-op', async () => {
50
50
  await store.removeChannels([]);
51
51
  });
52
+ test('session round-trip — set / get / clear', async () => {
53
+ await store.addChannel({
54
+ channelId: 'ch-state',
55
+ channelName: 'test-channel',
56
+ });
57
+ // initially undefined
58
+ const empty = await store.getState('ch-state');
59
+ assert.equal(empty, undefined);
60
+ // set + get
61
+ const payload = { userId: 'u-7', meta: { foo: 1 } };
62
+ await store.setState('ch-state', payload);
63
+ const got = await store.getState('ch-state');
64
+ assert.deepEqual(got, payload);
65
+ // overwrite
66
+ const next = { userId: 'u-8' };
67
+ await store.setState('ch-state', next);
68
+ const got2 = await store.getState('ch-state');
69
+ assert.deepEqual(got2, next);
70
+ // clear
71
+ await store.clearState('ch-state');
72
+ const after = await store.getState('ch-state');
73
+ assert.equal(after, undefined);
74
+ await store.removeChannels(['ch-state']);
75
+ });
52
76
  });
53
77
  }
54
78
  if (services.eventHubStore) {
@@ -5,6 +5,7 @@ import type { SchemaService } from '../services/schema-service.js';
5
5
  import type { JWTService } from '../services/jwt-service.js';
6
6
  import type { PikkuHTTP } from '../wirings/http/http.types.js';
7
7
  import type { PikkuChannel, CorePikkuChannelMiddleware, CorePikkuChannelMiddlewareFactory } from '../wirings/channel/channel.types.js';
8
+ import type { EventHubService } from '../wirings/channel/eventhub-service.js';
8
9
  import type { PikkuRPC } from '../wirings/rpc/rpc-types.js';
9
10
  import type { PikkuMCP } from '../wirings/mcp/mcp.types.js';
10
11
  import type { PikkuScheduledTask } from '../wirings/scheduler/scheduler.types.js';
@@ -82,6 +83,7 @@ export type FunctionRuntimeMeta = {
82
83
  version?: number;
83
84
  approvalRequired?: boolean;
84
85
  approvalDescription?: string;
86
+ implementationHash?: string;
85
87
  contractHash?: string;
86
88
  inputHash?: string;
87
89
  outputHash?: string;
@@ -162,6 +164,8 @@ export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
162
164
  workflowService?: WorkflowService;
163
165
  /** The queue service */
164
166
  queueService?: QueueService;
167
+ /** Event hub for realtime pub/sub across channels */
168
+ eventHub?: EventHubService<Record<string, any>>;
165
169
  /** The scheduler service */
166
170
  schedulerService?: SchedulerService;
167
171
  /** The deployment service for service discovery */
@@ -88,6 +88,7 @@ export interface PikkuPackageState {
88
88
  middleware: {
89
89
  tagGroup: Record<string, CorePikkuMiddlewareGroup>;
90
90
  httpGroup: Record<string, CorePikkuMiddlewareGroup>;
91
+ global: CorePikkuMiddlewareGroup;
91
92
  };
92
93
  channelMiddleware: {
93
94
  tagGroup: Record<string, CorePikkuChannelMiddleware[]>;
@@ -95,6 +96,7 @@ export interface PikkuPackageState {
95
96
  permissions: {
96
97
  tagGroup: Record<string, CorePermissionGroup | CorePikkuPermission[]>;
97
98
  httpGroup: Record<string, CorePermissionGroup | CorePikkuPermission[]>;
99
+ global: (CorePermissionGroup | CorePikkuPermission)[];
98
100
  };
99
101
  misc: {
100
102
  errors: Map<PikkuError, ErrorDetails>;
@@ -103,24 +105,6 @@ export interface PikkuPackageState {
103
105
  channelMiddleware: Record<string, CorePikkuChannelMiddleware[]>;
104
106
  permissions: Record<string, CorePermissionGroup | CorePikkuPermission[]>;
105
107
  };
106
- models: {
107
- config: {
108
- models?: Record<string, string | {
109
- model: string;
110
- temperature?: number;
111
- maxSteps?: number;
112
- }>;
113
- agentDefaults?: {
114
- temperature?: number;
115
- maxSteps?: number;
116
- };
117
- agentOverrides?: Record<string, {
118
- model?: string;
119
- temperature?: number;
120
- maxSteps?: number;
121
- }>;
122
- } | null;
123
- };
124
108
  package: {
125
109
  /** Service factory functions for addon packages */
126
110
  factories: {
@@ -3,14 +3,17 @@ import type { CoreAIAgent, AIAgentMemoryConfig, AIAgentInput, AIMessage } from '
3
3
  import type { AIStorageService } from '../../services/ai-storage-service.js';
4
4
  import type { Logger } from '../../services/logger.js';
5
5
  import type { SchemaService } from '../../services/schema-service.js';
6
+ import type { PikkuAIMiddlewareHooks } from './ai-agent.types.js';
6
7
  export declare function resolveMemoryServices(agent: CoreAIAgent, singletonServices: CoreSingletonServices): {
7
8
  storage: AIStorageService | undefined;
8
9
  };
10
+ export declare function isWorkingMemoryEnabled(memoryConfig: AIAgentMemoryConfig | undefined, storage: AIStorageService | undefined): boolean;
9
11
  export declare function deepMergeWorkingMemory(existing: Record<string, unknown>, updates: Record<string, unknown>): Record<string, unknown>;
10
12
  export declare function buildWorkingMemoryPrompt(currentState: Record<string, unknown> | null, jsonSchema?: Record<string, unknown>): string;
11
13
  export declare function loadContextMessages(memoryConfig: AIAgentMemoryConfig | undefined, storage: AIStorageService | undefined, input: AIAgentInput, workingMemoryJsonSchema?: Record<string, unknown>): Promise<AIMessage[]>;
12
14
  export declare function saveMessages(storage: AIStorageService | undefined, threadId: string, resourceId: string, memoryConfig: AIAgentMemoryConfig | undefined, userMessage: AIMessage | null | undefined, result: {
13
15
  text: string;
16
+ uiSpec?: unknown;
14
17
  steps: {
15
18
  toolCalls?: {
16
19
  name: string;
@@ -18,12 +21,28 @@ export declare function saveMessages(storage: AIStorageService | undefined, thre
18
21
  result: string;
19
22
  }[];
20
23
  }[];
21
- }, options?: {
22
- workingMemoryJsonSchema?: Record<string, unknown>;
23
- workingMemorySchemaName?: string | null;
24
- logger?: Logger;
25
- schemaService?: SchemaService;
26
24
  }): Promise<string>;
27
25
  export declare function trimMessages(messages: AIMessage[], maxTokenBudget?: number): AIMessage[];
28
26
  export declare function parseWorkingMemory(text: string): Record<string, unknown> | null;
29
27
  export declare function stripWorkingMemoryTags(text: string): string;
28
+ export declare function extractWorkingMemory(text: string): {
29
+ workingMemory: Record<string, unknown> | null;
30
+ cleanedText: string;
31
+ };
32
+ export declare function stripWorkingMemoryForStreaming(text: string): string;
33
+ export declare function createWorkingMemoryMiddleware(options: {
34
+ storage?: AIStorageService;
35
+ threadId: string;
36
+ workingMemorySchemaName?: string | null;
37
+ logger?: Logger;
38
+ schemaService?: SchemaService;
39
+ }): PikkuAIMiddlewareHooks<{
40
+ rawText?: string;
41
+ emittedVisibleText?: string;
42
+ }>;
43
+ export declare function getWorkingMemoryMiddleware(memoryConfig: AIAgentMemoryConfig | undefined, storage: AIStorageService | undefined, options: {
44
+ threadId: string;
45
+ workingMemorySchemaName?: string | null;
46
+ logger?: Logger;
47
+ schemaService?: SchemaService;
48
+ }): PikkuAIMiddlewareHooks[];
@@ -6,9 +6,15 @@ export function resolveMemoryServices(agent, singletonServices) {
6
6
  : singletonServices.aiStorage;
7
7
  return { storage };
8
8
  }
9
+ export function isWorkingMemoryEnabled(memoryConfig, storage) {
10
+ return !!memoryConfig?.workingMemory && !!storage;
11
+ }
9
12
  export function deepMergeWorkingMemory(existing, updates) {
10
13
  const result = { ...existing };
11
14
  for (const key of Object.keys(updates)) {
15
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
16
+ continue;
17
+ }
12
18
  const value = updates[key];
13
19
  if (value === null) {
14
20
  delete result[key];
@@ -46,13 +52,18 @@ export function buildWorkingMemoryPrompt(currentState, jsonSchema) {
46
52
  parts.push('Current working memory: (empty)');
47
53
  }
48
54
  parts.push('When you learn new information, output a partial JSON update in <working_memory> tags. ' +
49
- 'Only include changed fields. Set a field to null to delete it.');
55
+ 'Only include durable facts you have actually derived or the user has confirmed. ' +
56
+ 'Do not output templates, placeholders, or narration. ' +
57
+ 'Only include changed fields. Leave unknown fields untouched. Set a field to null to delete it.');
50
58
  return parts.join('\n\n');
51
59
  }
52
60
  export async function loadContextMessages(memoryConfig, storage, input, workingMemoryJsonSchema) {
53
61
  const contextMessages = [];
54
- if (memoryConfig?.workingMemory && storage) {
55
- const workingMem = await storage.getWorkingMemory(input.threadId, 'thread');
62
+ const workingMemoryStorage = isWorkingMemoryEnabled(memoryConfig, storage)
63
+ ? storage
64
+ : undefined;
65
+ if (workingMemoryStorage) {
66
+ const workingMem = await workingMemoryStorage.getWorkingMemory(input.threadId, 'thread');
56
67
  const prompt = buildWorkingMemoryPrompt(workingMem, workingMemoryJsonSchema);
57
68
  contextMessages.push({
58
69
  id: `wm-${randomUUID()}`,
@@ -63,8 +74,10 @@ export async function loadContextMessages(memoryConfig, storage, input, workingM
63
74
  }
64
75
  return contextMessages;
65
76
  }
66
- export async function saveMessages(storage, threadId, resourceId, memoryConfig, userMessage, result, options) {
67
- let responseText = result.text;
77
+ export async function saveMessages(storage, threadId, resourceId, memoryConfig, userMessage, result) {
78
+ const responseText = memoryConfig?.workingMemory
79
+ ? extractWorkingMemory(result.text).cleanedText
80
+ : result.text;
68
81
  if (storage) {
69
82
  const newMessages = userMessage ? [userMessage] : [];
70
83
  for (const step of result.steps) {
@@ -92,30 +105,21 @@ export async function saveMessages(storage, threadId, resourceId, memoryConfig,
92
105
  });
93
106
  }
94
107
  }
108
+ const assistantContent = result.uiSpec != null
109
+ ? [
110
+ ...(responseText
111
+ ? [{ type: 'text', text: responseText }]
112
+ : []),
113
+ { type: 'generative-ui', spec: result.uiSpec },
114
+ ]
115
+ : responseText;
95
116
  newMessages.push({
96
117
  id: randomUUID(),
97
118
  role: 'assistant',
98
- content: responseText,
119
+ content: assistantContent || undefined,
99
120
  createdAt: new Date(),
100
121
  });
101
122
  await storage.saveMessages(threadId, newMessages);
102
- if (memoryConfig?.workingMemory) {
103
- const parsed = parseWorkingMemory(responseText);
104
- if (parsed) {
105
- const existing = (await storage.getWorkingMemory(threadId, 'thread')) ?? {};
106
- const merged = deepMergeWorkingMemory(existing, parsed);
107
- if (options?.schemaService && options?.workingMemorySchemaName) {
108
- try {
109
- await options.schemaService.validateSchema(options.workingMemorySchemaName, merged);
110
- }
111
- catch (err) {
112
- options.logger?.warn(`Working memory validation failed: ${err instanceof Error ? err.message : String(err)}`);
113
- }
114
- }
115
- await storage.saveWorkingMemory(threadId, 'thread', merged);
116
- responseText = stripWorkingMemoryTags(responseText);
117
- }
118
- }
119
123
  }
120
124
  return responseText;
121
125
  }
@@ -209,3 +213,104 @@ export function parseWorkingMemory(text) {
209
213
  export function stripWorkingMemoryTags(text) {
210
214
  return text.replace(/<working_memory>[\s\S]*?<\/working_memory>/g, '').trim();
211
215
  }
216
+ export function extractWorkingMemory(text) {
217
+ const workingMemory = parseWorkingMemory(text);
218
+ return {
219
+ workingMemory,
220
+ cleanedText: workingMemory ? stripWorkingMemoryTags(text) : text,
221
+ };
222
+ }
223
+ function getPendingWorkingMemoryPrefixLength(text) {
224
+ const prefixes = ['<working_memory>', '</working_memory>'];
225
+ const lastLt = text.lastIndexOf('<');
226
+ if (lastLt === -1)
227
+ return 0;
228
+ const suffix = text.slice(lastLt);
229
+ for (const prefix of prefixes) {
230
+ if (prefix.startsWith(suffix)) {
231
+ return suffix.length;
232
+ }
233
+ }
234
+ return 0;
235
+ }
236
+ export function stripWorkingMemoryForStreaming(text) {
237
+ let output = '';
238
+ let cursor = 0;
239
+ while (cursor < text.length) {
240
+ const openIndex = text.indexOf('<working_memory>', cursor);
241
+ if (openIndex === -1) {
242
+ const tail = text.slice(cursor);
243
+ const pendingPrefixLength = getPendingWorkingMemoryPrefixLength(tail);
244
+ output +=
245
+ pendingPrefixLength > 0 ? tail.slice(0, -pendingPrefixLength) : tail;
246
+ return output;
247
+ }
248
+ output += text.slice(cursor, openIndex);
249
+ const closeIndex = text.indexOf('</working_memory>', openIndex);
250
+ if (closeIndex === -1) {
251
+ return output;
252
+ }
253
+ cursor = closeIndex + '</working_memory>'.length;
254
+ }
255
+ return output;
256
+ }
257
+ export function createWorkingMemoryMiddleware(options) {
258
+ return {
259
+ modifyOutputStream: async (_services, { event, state }) => {
260
+ if (event.type !== 'text-delta')
261
+ return event;
262
+ const rawText = `${state.rawText ?? ''}${event.text}`;
263
+ state.rawText = rawText;
264
+ const visibleText = stripWorkingMemoryForStreaming(rawText);
265
+ const emittedVisibleText = state.emittedVisibleText ?? '';
266
+ if (!visibleText.startsWith(emittedVisibleText)) {
267
+ state.emittedVisibleText = visibleText;
268
+ return { ...event, text: visibleText };
269
+ }
270
+ const delta = visibleText.slice(emittedVisibleText.length);
271
+ state.emittedVisibleText = visibleText;
272
+ if (!delta)
273
+ return null;
274
+ return { ...event, text: delta };
275
+ },
276
+ modifyOutput: async (_services, { text, messages, usage }) => {
277
+ const { workingMemory, cleanedText } = extractWorkingMemory(text);
278
+ if (workingMemory && options.storage) {
279
+ const existing = (await options.storage.getWorkingMemory(options.threadId, 'thread')) ?? {};
280
+ const merged = deepMergeWorkingMemory(existing, workingMemory);
281
+ let valid = true;
282
+ if (options.schemaService && options.workingMemorySchemaName) {
283
+ try {
284
+ await options.schemaService.validateSchema(options.workingMemorySchemaName, merged);
285
+ }
286
+ catch (err) {
287
+ valid = false;
288
+ options.logger?.warn(`Working memory validation failed: ${err instanceof Error ? err.message : String(err)}`);
289
+ }
290
+ }
291
+ // Only persist when the merged value passes schema validation —
292
+ // saving invalid data would poison subsequent getWorkingMemory reads.
293
+ if (valid) {
294
+ await options.storage.saveWorkingMemory(options.threadId, 'thread', merged);
295
+ }
296
+ }
297
+ return {
298
+ text: cleanedText,
299
+ messages,
300
+ };
301
+ },
302
+ };
303
+ }
304
+ export function getWorkingMemoryMiddleware(memoryConfig, storage, options) {
305
+ if (!isWorkingMemoryEnabled(memoryConfig, storage))
306
+ return [];
307
+ return [
308
+ createWorkingMemoryMiddleware({
309
+ storage,
310
+ threadId: options.threadId,
311
+ workingMemorySchemaName: options.workingMemorySchemaName,
312
+ logger: options.logger,
313
+ schemaService: options.schemaService,
314
+ }),
315
+ ];
316
+ }
@@ -1,4 +1,13 @@
1
- export declare function resolveModelConfig(agentName: string, agent: {
1
+ /**
2
+ * Resolves the effective model/temperature/maxSteps for an agent at runtime.
3
+ *
4
+ * Models are declared per-agent using the provider-qualified `provider/model`
5
+ * form (e.g. `openai/gpt-5-mini`); there is no config-level alias map. This
6
+ * function remains the single merge point for request-time overrides (see
7
+ * ai-agent-prepare's `input.model`), so callers stay decoupled from how the
8
+ * effective config is assembled.
9
+ */
10
+ export declare function resolveModelConfig(_agentName: string, agent: {
2
11
  model: string;
3
12
  temperature?: number;
4
13
  maxSteps?: number;
@@ -1,36 +1,16 @@
1
- import { pikkuState } from '../../pikku-state.js';
2
- export function resolveModelConfig(agentName, agent) {
3
- let config = {};
4
- try {
5
- config = pikkuState(null, 'models', 'config') ?? {};
6
- }
7
- catch { }
8
- const models = config.models ?? {};
9
- const defaults = config.agentDefaults;
10
- const agentOverride = config.agentOverrides?.[agentName];
11
- let rawModel = agentOverride?.model ?? agent.model;
12
- let aliasTemperature;
13
- let aliasMaxSteps;
14
- if (!rawModel.includes('/')) {
15
- const entry = models[rawModel];
16
- if (!entry)
17
- throw new Error(`Unknown model alias '${rawModel}'.`);
18
- if (typeof entry === 'string') {
19
- rawModel = entry;
20
- }
21
- else {
22
- rawModel = entry.model;
23
- aliasTemperature = entry.temperature;
24
- aliasMaxSteps = entry.maxSteps;
25
- }
26
- }
27
- const temperature = agentOverride?.temperature ??
28
- aliasTemperature ??
29
- defaults?.temperature ??
30
- agent.temperature;
31
- const maxSteps = agentOverride?.maxSteps ??
32
- aliasMaxSteps ??
33
- defaults?.maxSteps ??
34
- agent.maxSteps;
35
- return { model: rawModel, temperature, maxSteps };
1
+ /**
2
+ * Resolves the effective model/temperature/maxSteps for an agent at runtime.
3
+ *
4
+ * Models are declared per-agent using the provider-qualified `provider/model`
5
+ * form (e.g. `openai/gpt-5-mini`); there is no config-level alias map. This
6
+ * function remains the single merge point for request-time overrides (see
7
+ * ai-agent-prepare's `input.model`), so callers stay decoupled from how the
8
+ * effective config is assembled.
9
+ */
10
+ export function resolveModelConfig(_agentName, agent) {
11
+ return {
12
+ model: agent.model,
13
+ temperature: agent.temperature,
14
+ maxSteps: agent.maxSteps,
15
+ };
36
16
  }
@@ -169,6 +169,9 @@ export function createScopedChannel(parent, agentName, session) {
169
169
  parent.send(event);
170
170
  }
171
171
  },
172
+ setState: (s) => parent.setState(s),
173
+ getState: () => parent.getState(),
174
+ clearState: () => parent.clearState(),
172
175
  };
173
176
  }
174
177
  export async function buildToolDefs(params, agentSessionMap, resourceId, agentName, packageName, streamContext, aiMiddlewares, agentMode) {
@@ -254,7 +257,7 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
254
257
  }
255
258
  }
256
259
  tools.push({
257
- name: toolName.replaceAll(':', '__'),
260
+ name: toolName.replaceAll('@', '_').replaceAll(':', '__'),
258
261
  description: fnMeta?.description || fnMeta?.title || toolName,
259
262
  inputSchema,
260
263
  needsApproval: needsApproval || undefined,