@pikku/core 0.12.92 → 0.12.94

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 (107) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/dist/errors/error-handler.d.ts +11 -0
  3. package/dist/errors/error-handler.js +8 -0
  4. package/dist/errors/serialized-error.d.ts +3 -0
  5. package/dist/function/function-runner.d.ts +1 -1
  6. package/dist/function/function-runner.js +3 -3
  7. package/dist/function/functions.types.d.ts +33 -21
  8. package/dist/middleware/auth-apikey.d.ts +7 -0
  9. package/dist/middleware/auth-apikey.js +6 -0
  10. package/dist/middleware/auth-bearer.d.ts +3 -0
  11. package/dist/middleware/auth-bearer.js +2 -0
  12. package/dist/middleware/auth-cookie.d.ts +9 -0
  13. package/dist/middleware/auth-cookie.js +6 -4
  14. package/dist/middleware/cors.d.ts +8 -0
  15. package/dist/middleware/cors.js +2 -0
  16. package/dist/middleware/middleware-factories.d.ts +14 -0
  17. package/dist/middleware/middleware-factories.js +14 -0
  18. package/dist/services/local-secrets.js +1 -1
  19. package/dist/services/personas-service.d.ts +4 -0
  20. package/dist/services/personas-service.js +4 -0
  21. package/dist/services/secret-service.d.ts +0 -1
  22. package/dist/testing/service-tests/secret-service-tests.js +1 -1
  23. package/dist/types/core.types.d.ts +5 -0
  24. package/dist/wirings/addon/wire-addon.d.ts +17 -15
  25. package/dist/wirings/addon/wire-addon.js +6 -0
  26. package/dist/wirings/addon/wire-remote-addon.d.ts +4 -0
  27. package/dist/wirings/addon/wire-remote-addon.js +4 -0
  28. package/dist/wirings/agent/agent-prepare.js +2 -2
  29. package/dist/wirings/agent/agent.types.d.ts +31 -27
  30. package/dist/wirings/agent-scorer/agent-scorer.d.ts +4 -0
  31. package/dist/wirings/agent-scorer/agent-scorer.js +4 -0
  32. package/dist/wirings/channel/channel.types.d.ts +11 -0
  33. package/dist/wirings/cli/cli.types.d.ts +43 -1
  34. package/dist/wirings/cli/define-cli-commands.d.ts +6 -1
  35. package/dist/wirings/cli/define-cli-commands.js +6 -1
  36. package/dist/wirings/credential/credential.types.d.ts +7 -0
  37. package/dist/wirings/gateway/gateway.types.d.ts +23 -0
  38. package/dist/wirings/http/http.types.d.ts +51 -37
  39. package/dist/wirings/mcp/mcp.types.d.ts +27 -0
  40. package/dist/wirings/persona/index.d.ts +1 -0
  41. package/dist/wirings/persona/index.js +1 -0
  42. package/dist/wirings/persona/persona-app-scopes.d.ts +41 -0
  43. package/dist/wirings/persona/persona-app-scopes.js +61 -0
  44. package/dist/wirings/queue/queue.types.d.ts +6 -0
  45. package/dist/wirings/scheduler/scheduler.types.d.ts +5 -0
  46. package/dist/wirings/secret/secret.types.d.ts +9 -14
  47. package/dist/wirings/secret/secret.types.js +7 -0
  48. package/dist/wirings/trigger/trigger.types.d.ts +4 -1
  49. package/dist/wirings/variable/variable.types.d.ts +14 -13
  50. package/dist/wirings/variable/variable.types.js +7 -0
  51. package/dist/wirings/workflow/graph/wire-workflow-graph.d.ts +7 -0
  52. package/dist/wirings/workflow/scenario-cookie-jar.d.ts +6 -0
  53. package/dist/wirings/workflow/scenario-cookie-jar.js +6 -0
  54. package/dist/wirings/workflow/scenario-poll.d.ts +6 -0
  55. package/dist/wirings/workflow/scenario-poll.js +6 -0
  56. package/dist/wirings/workflow/scenario-step-guards.d.ts +6 -0
  57. package/dist/wirings/workflow/scenario-step-guards.js +6 -0
  58. package/dist/wirings/workflow/workflow-errors.d.ts +4 -0
  59. package/dist/wirings/workflow/workflow-errors.js +4 -0
  60. package/package.json +1 -1
  61. package/src/app-leaf-surface.test.ts +2 -2
  62. package/src/errors/error-handler.ts +11 -0
  63. package/src/errors/serialized-error.ts +3 -0
  64. package/src/function/function-runner.ts +4 -4
  65. package/src/function/functions.types.ts +33 -21
  66. package/src/middleware/auth-apikey.ts +7 -0
  67. package/src/middleware/auth-bearer.ts +3 -0
  68. package/src/middleware/auth-cookie.ts +9 -4
  69. package/src/middleware/cors.ts +8 -0
  70. package/src/middleware/middleware-factories.ts +14 -0
  71. package/src/public-surface.json +3 -0
  72. package/src/services/local-secrets.test.ts +2 -2
  73. package/src/services/local-secrets.ts +1 -1
  74. package/src/services/personas-service.ts +4 -0
  75. package/src/services/secret-service.ts +0 -1
  76. package/src/testing/service-tests/secret-service-tests.ts +1 -1
  77. package/src/types/core.types.ts +5 -0
  78. package/src/wirings/addon/addon-auth-tags.test.ts +4 -4
  79. package/src/wirings/addon/wire-addon.ts +17 -15
  80. package/src/wirings/addon/wire-remote-addon.ts +4 -0
  81. package/src/wirings/agent/agent-authorization.test.ts +7 -3
  82. package/src/wirings/agent/agent-prepare.ts +2 -2
  83. package/src/wirings/agent/agent.types.ts +31 -27
  84. package/src/wirings/agent-scorer/agent-scorer.ts +4 -0
  85. package/src/wirings/channel/channel.types.ts +11 -0
  86. package/src/wirings/cli/cli.types.ts +44 -1
  87. package/src/wirings/cli/define-cli-commands.ts +6 -1
  88. package/src/wirings/credential/credential.types.ts +7 -0
  89. package/src/wirings/gateway/gateway-authorization.test.ts +3 -3
  90. package/src/wirings/gateway/gateway.types.ts +23 -0
  91. package/src/wirings/http/http-runner-addon-ref.test.ts +1 -1
  92. package/src/wirings/http/http.types.ts +94 -69
  93. package/src/wirings/mcp/mcp.types.ts +27 -0
  94. package/src/wirings/persona/index.ts +5 -0
  95. package/src/wirings/persona/persona-app-scopes.test.ts +47 -0
  96. package/src/wirings/persona/persona-app-scopes.ts +74 -0
  97. package/src/wirings/queue/queue.types.ts +6 -0
  98. package/src/wirings/scheduler/scheduler.types.ts +5 -0
  99. package/src/wirings/secret/secret.types.ts +9 -14
  100. package/src/wirings/trigger/trigger.types.ts +4 -1
  101. package/src/wirings/variable/variable.types.ts +14 -13
  102. package/src/wirings/workflow/graph/wire-workflow-graph.ts +7 -0
  103. package/src/wirings/workflow/scenario-cookie-jar.ts +6 -0
  104. package/src/wirings/workflow/scenario-poll.ts +6 -0
  105. package/src/wirings/workflow/scenario-step-guards.ts +6 -0
  106. package/src/wirings/workflow/workflow-errors.ts +4 -0
  107. package/tsconfig.tsbuildinfo +1 -1
@@ -8,13 +8,21 @@ import {
8
8
  * matching origin and otherwise sends no `Access-Control-Allow-Origin` at all,
9
9
  * so the browser reports "origin not allowed" rather than an origin mismatch
10
10
  * against whichever entry happened to be first.
11
+ *
12
+ * @example snippet: corsMiddleware
11
13
  */
12
14
  export const cors = pikkuMiddlewareFactory<{
15
+ /** Which origins may call. Defaults to `*`, which the browser rejects alongside `credentials: true` — name the origins instead. */
13
16
  origin?: string | string[] | true
17
+ /** Methods a cross-origin caller may use. Defaults to the common six; a method missing here fails preflight rather than the request. */
14
18
  methods?: string[]
19
+ /** Request headers a caller may send. Defaults to content-type, authorization and x-api-key. */
15
20
  headers?: string[]
21
+ /** Response headers the browser will let the caller's JavaScript read. Everything else is hidden from it even on a 200. */
16
22
  exposeHeaders?: string[]
23
+ /** Whether cookies and auth headers ride along. Requires a named origin, never `*`. */
17
24
  credentials?: boolean
25
+ /** Seconds the browser may cache this preflight. Defaults to a day. */
18
26
  maxAge?: number
19
27
  }>(
20
28
  ({
@@ -39,6 +39,10 @@ export const pikkuMiddlewareFactory = <In = any>(
39
39
  return factory
40
40
  }
41
41
 
42
+ /**
43
+ * Declares middleware for a channel — it runs around the connection and its
44
+ * messages rather than around a single request.
45
+ */
42
46
  export const pikkuChannelMiddleware = <
43
47
  SingletonServices extends CoreSingletonServices = CoreSingletonServices,
44
48
  Event = unknown,
@@ -48,12 +52,22 @@ export const pikkuChannelMiddleware = <
48
52
  return middleware
49
53
  }
50
54
 
55
+ /**
56
+ * Declares channel middleware that takes options, so one definition can be
57
+ * wired several times with different configuration.
58
+ *
59
+ * @example snippet: channelMiddlewareFactory
60
+ */
51
61
  export const pikkuChannelMiddlewareFactory = <In = any>(
52
62
  factory: CorePikkuChannelMiddlewareFactory<In>
53
63
  ): CorePikkuChannelMiddlewareFactory<In> => {
54
64
  return factory
55
65
  }
56
66
 
67
+ /**
68
+ * Declares middleware for an agent run — hooks around the model call, its tool
69
+ * calls and the run's state.
70
+ */
57
71
  export const pikkuAgentMiddleware = <
58
72
  State extends Record<string, unknown> = Record<string, unknown>,
59
73
  SingletonServices extends CoreSingletonServices = CoreSingletonServices,
@@ -272,10 +272,13 @@
272
272
  "validateAndBuildSystemRoleDefinitionsMeta"
273
273
  ],
274
274
  "./persona": [
275
+ "APP_SCOPE_ROOT",
275
276
  "ActorSignIn",
276
277
  "HttpPersona",
277
278
  "IMPERSONATE_USER_ID_HEADER",
278
279
  "OperatorSignIn",
280
+ "appScopeId",
281
+ "buildAppScopeDefinition",
279
282
  "createHttpPersonas",
280
283
  "definePersonas",
281
284
  "establishOperatorSession",
@@ -22,7 +22,7 @@ describe('LocalSecretService', () => {
22
22
  const vars = new LocalVariablesService({})
23
23
  const service = new LocalSecretService(vars)
24
24
  await assert.rejects(() => service.getSecret('MISSING'), {
25
- message: 'Requested secret not found',
25
+ message: 'Requested secret not found: MISSING',
26
26
  })
27
27
  })
28
28
 
@@ -44,7 +44,7 @@ describe('LocalSecretService', () => {
44
44
  const vars = new LocalVariablesService({})
45
45
  const service = new LocalSecretService(vars)
46
46
  await assert.rejects(() => service.getSecret('MISSING'), {
47
- message: 'Requested secret not found',
47
+ message: 'Requested secret not found: MISSING',
48
48
  })
49
49
  })
50
50
 
@@ -32,7 +32,7 @@ export class LocalSecretService implements SecretService {
32
32
  if (value) {
33
33
  return createSecretValue(this.parseSecret<T>(value))
34
34
  }
35
- throw new Error('Requested secret not found')
35
+ throw new Error(`Requested secret not found: ${key}`)
36
36
  }
37
37
 
38
38
  public async setSecret(key: string, value: unknown): Promise<void> {
@@ -38,6 +38,8 @@ export interface ScenarioHttpResponse<T = unknown> {
38
38
  * `invokeRaw` returns this, and a step that has to reach past a persona — a
39
39
  * route with no RPC, an identity no persona can hold — reaches for this rather
40
40
  * than writing the same record by hand.
41
+ *
42
+ * @example snippet: scenarioPolling
41
43
  */
42
44
  export const readScenarioHttpResponse = async <T = unknown>(
43
45
  res: Response
@@ -84,6 +86,8 @@ export interface ScenarioJsonRequest {
84
86
  * outright when the target answers an empty body or an HTML error page. A
85
87
  * refusal is the expected outcome of a permissions scenario, so it has to
86
88
  * survive as data.
89
+ *
90
+ * @example snippet: scenarioHttpStep
87
91
  */
88
92
  export const postScenarioJson = async <T = unknown>(
89
93
  url: string,
@@ -4,7 +4,6 @@ import type { SecretValue } from '../classification/secret-value.js'
4
4
  export type SecretValues<T> = { [K in keyof T]: SecretValue<T[K]> }
5
5
 
6
6
  export interface SecretService {
7
- /** Throws if the secret is not found. Unwrap the result with `.reveal()`. */
8
7
  getSecret<T = string>(key: string): Promise<SecretValue<T>>
9
8
  /** Answers for any key, including a disallowed one — it must not throw. */
10
9
  hasSecret(key: string): Promise<boolean>
@@ -44,7 +44,7 @@ export const defineSecretServiceTests = (
44
44
  test('getSecret throws for missing key', async () => {
45
45
  const service = await factory({ key: kek })
46
46
  await assert.rejects(() => service.getSecret('nonexistent'), {
47
- message: 'Requested secret not found',
47
+ message: 'Requested secret not found: nonexistent',
48
48
  })
49
49
  })
50
50
 
@@ -373,10 +373,15 @@ export type CommonWireMeta = {
373
373
  pikkuFuncId: string
374
374
  packageName?: string
375
375
 
376
+ /** A human name for this wiring, shown wherever it is listed rather than called. */
376
377
  title?: string
378
+ /** Filters this wiring in and out of a build — see the `tags` option on `pikku all`. It has no effect at runtime. */
377
379
  tags?: string[]
380
+ /** A one-line description for listings, where the full `description` is too long. */
378
381
  summary?: string
382
+ /** What this does, for whoever is reading the wiring rather than writing it. */
379
383
  description?: string
384
+ /** Names of error classes this may throw, so each one's registered status is used instead of a 500. */
380
385
  errors?: string[]
381
386
 
382
387
  middleware?: MiddlewareMetadata[]
@@ -7,7 +7,7 @@ import {
7
7
  addTagMiddleware,
8
8
  clearMiddlewareCache,
9
9
  } from '../../middleware-runner.js'
10
- import { ForbiddenError } from '../../errors/errors.js'
10
+ import { ForbiddenError, MissingSessionError } from '../../errors/errors.js'
11
11
  import { resolveAddonAuth, resolveAddonTags, wireAddon } from './wire-addon.js'
12
12
 
13
13
  const ADDON_PACKAGE = '@addon/console'
@@ -114,7 +114,7 @@ describe('wireAddon auth on direct wirings', () => {
114
114
 
115
115
  await assert.rejects(
116
116
  () => callOverDirectWiring('credentialGet'),
117
- ForbiddenError
117
+ MissingSessionError
118
118
  )
119
119
  })
120
120
 
@@ -141,7 +141,7 @@ describe('wireAddon auth on direct wirings', () => {
141
141
 
142
142
  await assert.rejects(
143
143
  () => callOverDirectWiring('credentialGet', { auth: true }),
144
- ForbiddenError
144
+ MissingSessionError
145
145
  )
146
146
  })
147
147
 
@@ -161,7 +161,7 @@ describe('wireAddon auth on direct wirings', () => {
161
161
  ]) {
162
162
  await assert.rejects(
163
163
  () => callOverDirectWiring('credentialGet', { wireType }),
164
- ForbiddenError,
164
+ MissingSessionError,
165
165
  `${wireType} should have been gated`
166
166
  )
167
167
  }
@@ -2,35 +2,31 @@ import { pikkuState } from '../../pikku-state.js'
2
2
  import { getTagGroups } from '../../utils.js'
3
3
  import type { CorePikkuMiddleware } from '../../middleware/middleware.types.js'
4
4
  export type WireAddonConfig = {
5
+ /** How this instance is addressed. One package may be wired more than once, and the name is what tells the instances apart. */
5
6
  name: string
7
+ /** The npm package the addon ships in. */
6
8
  package: string
9
+ /** Where to reach the addon when it runs as its own service rather than in-process. */
7
10
  rpcEndpoint?: string
11
+ /** Requires a session for every function in the addon, whatever each one declares. Gates an addon whose functions are individually open. */
8
12
  auth?: boolean
13
+ /** Offers the addon's functions to MCP clients as tools, without wiring each one. */
9
14
  mcp?: boolean
15
+ /** Filters this addon in and out of a build — see the `tags` option on `pikku all`. It has no effect at runtime. */
10
16
  tags?: string[]
11
17
  /** Required of every function in the addon, on top of the function's own. */
12
18
  scopes?: string[]
19
+ /** Points a secret the addon reads at a different key in this deployment, so two instances can hold different credentials. */
13
20
  secretOverrides?: Record<string, string>
21
+ /** Points a variable the addon reads at a different key in this deployment. */
14
22
  variableOverrides?: Record<string, string>
23
+ /** Points a credential the addon reads at a different key in this deployment. */
15
24
  credentialOverrides?: Record<string, string>
16
- /**
17
- * Secrets this instance may read on top of the ones it declared, named as the
18
- * addon reads them — the scope check runs before `secretOverrides` renames
19
- * them, so an overridden secret is named here by its addon-side key. Listing
20
- * one in `secretOverrides` grants it too.
21
- *
22
- * For an addon whose secret names come off its input rather than its own
23
- * source, this is how the host lends names the addon could not declare.
24
- */
25
+ /** Extra secrets this instance may read, named as the addon reads them — the scope check runs before `secretOverrides` renames them. */
25
26
  secretGrants?: string[]
26
27
  /** Credentials this instance may read on top of the ones it declared. */
27
28
  credentialGrants?: string[]
28
- /**
29
- * Hands this instance the whole `SecretService` instead of one scoped to the
30
- * secrets it declared. The value is the reason, recorded in the deploy
31
- * manifest — an addon that names secrets at runtime cannot be scoped, and
32
- * only the consuming app, never the addon, can grant it.
33
- */
29
+ /** Hands over the whole `SecretService` unscoped. The value is the reason, recorded in the deploy manifest. */
34
30
  globalSecrets?: string
35
31
  /**
36
32
  * Hands this instance the whole `CredentialService` instead of one narrowed
@@ -40,6 +36,12 @@ export type WireAddonConfig = {
40
36
  globalCredentials?: string
41
37
  }
42
38
 
39
+ /**
40
+ * Installs an addon into this project: its functions, wirings and scopes become
41
+ * part of the app, under the namespace and options given here.
42
+ *
43
+ * @example snippet: addonWiring
44
+ */
43
45
  export const wireAddon = (config: WireAddonConfig): void => {
44
46
  pikkuState(null, 'addons', 'packages').set(config.name, {
45
47
  package: config.package,
@@ -25,6 +25,10 @@ export type WireRemoteAddonConfig = {
25
25
  tags?: string[]
26
26
  }
27
27
 
28
+ /**
29
+ * Installs an addon that runs as its own deployed service: the contract is
30
+ * local, the calls go over the wire to the addon's own host.
31
+ */
28
32
  export const wireRemoteAddon = (config: WireRemoteAddonConfig): void => {
29
33
  pikkuState(null, 'addons', 'packages').set(config.name, {
30
34
  package: config.package,
@@ -8,7 +8,11 @@ import {
8
8
  } from '../../pikku-state.js'
9
9
  import { assertAgentAuthorized } from './agent-prepare.js'
10
10
  import { clearPermissionsCache } from '../../permissions.js'
11
- import { ForbiddenError, MissingScopeError } from '../../errors/errors.js'
11
+ import {
12
+ ForbiddenError,
13
+ MissingScopeError,
14
+ MissingSessionError,
15
+ } from '../../errors/errors.js'
12
16
  import type { CoreAgent } from './agent.types.js'
13
17
  import type { CoreUserSession } from '../../types/core.types.js'
14
18
 
@@ -73,7 +77,7 @@ describe('assertAgentAuthorized', () => {
73
77
  await assertAgentAuthorized(agent, {}, null)
74
78
  })
75
79
 
76
- test('throws ForbiddenError when auth is explicitly true and no session exists', async () => {
80
+ test('throws MissingSessionError when auth is explicitly true and no session exists', async () => {
77
81
  const agent = addAgent('needs-session', { auth: true })
78
82
  await assert.rejects(
79
83
  () =>
@@ -82,7 +86,7 @@ describe('assertAgentAuthorized', () => {
82
86
  { sessionService: sessionService(undefined) },
83
87
  null
84
88
  ),
85
- ForbiddenError
89
+ MissingSessionError
86
90
  )
87
91
  })
88
92
 
@@ -22,7 +22,7 @@ import {
22
22
  type PermissionWire,
23
23
  } from '../../permissions.js'
24
24
  import { AIProviderNotConfiguredError } from '../../errors/errors.js'
25
- import { ForbiddenError } from '../../errors/errors.js'
25
+ import { ForbiddenError, MissingSessionError } from '../../errors/errors.js'
26
26
  import { verifyScopes } from '../../scopes.js'
27
27
  import { pikkuState, getSingletonServices } from '../../pikku-state.js'
28
28
  import { createMiddlewareSessionWireProps } from '../../services/user-session-service.js'
@@ -315,7 +315,7 @@ export async function assertAgentAuthorized(
315
315
  : undefined
316
316
 
317
317
  if (agent.auth === true && !session) {
318
- throw new ForbiddenError('Authentication required')
318
+ throw new MissingSessionError('Authentication required')
319
319
  }
320
320
 
321
321
  verifyScopes(agent.scopes, session)
@@ -183,6 +183,7 @@ export interface PikkuAgentMiddlewareHooks<
183
183
  State extends Record<string, unknown> = Record<string, unknown>,
184
184
  SingletonServices extends CoreSingletonServices = CoreSingletonServices,
185
185
  > {
186
+ /** Rewrites what the model is about to see — messages and instructions — before each turn. */
186
187
  modifyInput?: (
187
188
  services: SingletonServices,
188
189
  ctx: {
@@ -210,6 +211,7 @@ export interface PikkuAgentMiddlewareHooks<
210
211
  | Promise<{ messages: AgentMessage[]; instructions: string }>
211
212
  | { messages: AgentMessage[]; instructions: string }
212
213
 
214
+ /** Sees the model's output as it streams, for redaction or live inspection. Keeps its own `state` across chunks, unlike the shared run notes. */
213
215
  modifyOutputStream?: (
214
216
  services: SingletonServices,
215
217
  ctx: {
@@ -262,6 +264,7 @@ export interface PikkuAgentMiddlewareHooks<
262
264
  * and handed to anything that grades the run, and scrubbing the reply alone
263
265
  * leaves them untouched.
264
266
  */
267
+ /** Rewrites the finished output of a turn, after streaming has completed. */
265
268
  modifyOutput?: (
266
269
  services: SingletonServices,
267
270
  ctx: {
@@ -282,6 +285,7 @@ export interface PikkuAgentMiddlewareHooks<
282
285
  toolCalls?: NonNullable<AgentStep['toolCalls']>
283
286
  }
284
287
 
288
+ /** Runs before a tool executes, and may rewrite the arguments the model chose. Returning nothing leaves them as they are. */
285
289
  beforeToolCall?: (
286
290
  services: SingletonServices,
287
291
  ctx: {
@@ -294,6 +298,7 @@ export interface PikkuAgentMiddlewareHooks<
294
298
  | { args: Record<string, unknown> }
295
299
  | void
296
300
 
301
+ /** Runs once a tool has returned, and may replace its result before the model sees it. */
297
302
  afterToolCall?: (
298
303
  services: SingletonServices,
299
304
  ctx: {
@@ -305,6 +310,7 @@ export interface PikkuAgentMiddlewareHooks<
305
310
  }
306
311
  ) => Promise<{ result: unknown } | void> | { result: unknown } | void
307
312
 
313
+ /** Runs at the end of each model turn. For observation — it cannot change what happened. */
308
314
  afterStep?: (
309
315
  services: SingletonServices,
310
316
  ctx: {
@@ -323,6 +329,7 @@ export interface PikkuAgentMiddlewareHooks<
323
329
  }
324
330
  ) => Promise<void> | void
325
331
 
332
+ /** Runs when a turn throws, with the step it failed on. For logging and cleanup; it does not swallow the error. */
326
333
  onError?: (
327
334
  services: SingletonServices,
328
335
  ctx: {
@@ -347,58 +354,51 @@ export type CoreAgent<
347
354
  Scope extends string = string,
348
355
  Scorer extends string = string,
349
356
  > = {
357
+ /** Unique across the project. It is how the agent is invoked and how its runs are grouped. */
350
358
  name: string
359
+ /** What the agent is for. Another agent choosing whether to delegate to this one reads it, so write it for that reader. */
351
360
  description: string
361
+ /** A one-line description for listings, where the full `description` is too long. */
352
362
  summary?: string
363
+ /** Names of error classes this may throw, so each one's registered status is used instead of a 500. */
353
364
  errors?: string[]
354
- /**
355
- * The three fields below are the system prompt. `buildInstructions` joins
356
- * whichever are set with a blank line, always in this order — role, then
357
- * personality, then goal — and appends the tool-usage rules if the agent has
358
- * tools. Nothing checks them against each other: the split is there to keep a
359
- * prompt legible, and text put in the wrong one still reaches the model.
360
- *
361
- * Who the agent is. 'You are a support engineer triaging inbound bugs.'
362
- */
365
+ /** Who the agent is. Joined with `personality` and `goal`, in that order, to form the system prompt. */
363
366
  role?: string
364
367
  /** How it should sound: tone, vocabulary, how much it says at a time. */
365
368
  personality?: string
366
369
  /** What it is for — the only one of the three that is required. */
367
370
  goal: string
371
+ /** Which model to run on, as the provider names it. */
368
372
  model: string
373
+ /** How much the model is allowed to vary its answer. Lower is more repeatable, which is what a tool-driving agent usually wants. */
369
374
  temperature?: number
370
375
  /** Ownership/partitioning of this agent's threads and runs. Defaults to `'user'`. */
371
376
  sessionScope?: SessionScope
377
+ /** Functions the model may call, given to it as tools. A `readonly` one may be called without asking first. */
372
378
  tools?: unknown[]
379
+ /** Other agents this one may hand work to. `agentMode` decides whether it delegates or supervises. */
373
380
  agents?: unknown[]
381
+ /** Workflows the model may start, for work too long to sit inside one agent run. */
374
382
  workflows?: unknown[]
375
- /**
376
- * Grades this agent's finished runs on live traffic, named by the generated
377
- * `ScorerName` union rather than by `ref()` — a scorer is not a function, so
378
- * there is nothing in the function map for a ref to resolve against.
379
- *
380
- * A reference-based judge listed here is never sampled: live traffic has no
381
- * answer key. Scenarios name scorers directly and may grade with scorers an
382
- * agent does not ship with.
383
- */
383
+ /** Grades finished runs on live traffic. A reference-based judge is never sampled here — live traffic has no answer key. */
384
384
  scorers?: Scorer[]
385
+ /** `delegate` hands a sub-agent the task and takes its answer; `supervise` keeps this agent in the loop over each step. */
385
386
  agentMode?: 'delegate' | 'supervise'
387
+ /** What the agent carries between runs, and how much of it. */
386
388
  memory?: AgentMemoryConfig
389
+ /** How many model turns a single run may take before it is stopped. The guard against a tool loop that never converges. */
387
390
  maxSteps?: number
391
+ /** Whether the model may answer without calling a tool (`auto`), must call one (`required`), or may not (`none`). */
388
392
  toolChoice?: 'auto' | 'required' | 'none'
389
- /**
390
- * Per-provider model settings, keyed by provider id and passed through
391
- * untouched — for anything only one vendor offers, which the fields above
392
- * deliberately do not try to unify.
393
- *
394
- * `{ openai: { reasoningEffort: 'minimal' } }` is the one that matters for
395
- * voice: on gpt-5-mini it measured 0.9s to first token against 2.5s at the
396
- * default, and a spoken reply is waited through rather than skimmed.
397
- */
393
+ /** Per-provider settings passed through untouched, for what only one vendor offers. */
398
394
  providerOptions?: AIProviderOptions
395
+ /** The schema of what starts a run, which is also its input type. */
399
396
  input?: unknown
397
+ /** The schema the final answer must match, which is also its return type. Without one the agent returns free text. */
400
398
  output?: unknown
399
+ /** Filters this agent in and out of a build — see the `tags` option on `pikku all`. It has no effect at runtime. */
401
400
  tags?: string[]
401
+ /** Runs before each model turn, to change what that turn sees or to stop the run. This is where a step budget or a tool narrowing goes. */
402
402
  prepareStep?: (ctx: {
403
403
  stepNumber: number
404
404
  messages: AgentMessage[]
@@ -407,8 +407,11 @@ export type CoreAgent<
407
407
  model: string
408
408
  stop: () => void
409
409
  }) => void | Promise<void>
410
+ /** Wraps the whole run: auth, tracing, spend limits. */
410
411
  middleware?: PikkuMiddleware[]
412
+ /** Wraps each message when the agent is driven over a channel. */
411
413
  channelMiddleware?: CorePikkuChannelMiddleware<any, any>[]
414
+ /** Hooks around each step and each tool call — the place to inspect, rewrite or veto what the model is about to do. */
412
415
  agentMiddleware?: PikkuAgentMiddlewareHooks<any, any>[]
413
416
  /**
414
417
  * Whether a session is required to run this agent. Defaults to `false`, since
@@ -427,6 +430,7 @@ export type CoreAgent<
427
430
  * `#pikku/scopes`, so an undeclared scope is a compile error.
428
431
  */
429
432
  scopes?: Scope[]
433
+ /** Checks that run before the agent starts. Grouped names OR together, so any one passing admits the caller. */
430
434
  permissions?: CorePermissionGroup<PikkuPermission>
431
435
  }
432
436
 
@@ -18,6 +18,8 @@ const assertSampleRate = (name: string, sampleRate: number | undefined) => {
18
18
  /**
19
19
  * A heuristic scorer: pure code over the finished run, no model call, so it
20
20
  * grades on the fast lane.
21
+ *
22
+ * @example snippet: agentScorer
21
23
  */
22
24
  export const pikkuAgentScorer = <Services = any>(config: {
23
25
  name: string
@@ -49,6 +51,8 @@ export const pikkuAgentScorer = <Services = any>(config: {
49
51
  * The rubric field is `goal`, matching `pikkuAgent`'s prompt vocabulary — a
50
52
  * judge is a degenerate agent, and should use the same word for the same thing.
51
53
  * `prompt` is the escape hatch for non-standard framing.
54
+ *
55
+ * @example snippet: agentJudge
52
56
  */
53
57
  export const pikkuAgentJudge = <Services = any>(config: {
54
58
  name: string
@@ -98,8 +98,11 @@ export type CoreChannel<
98
98
  PikkuPermission = CorePikkuPermission<ChannelData>,
99
99
  PikkuMiddleware = CorePikkuMiddleware,
100
100
  > = {
101
+ /** Unique across the project. It is how the channel is addressed in `pikku meta` and by the generated client. */
101
102
  name: string
103
+ /** The path a client opens the socket on. */
102
104
  route: Channel
105
+ /** Runs once when a client connects, before any message. Its return value is sent as the first message. */
103
106
  onConnect?:
104
107
  | ChannelConnect
105
108
  | {
@@ -112,7 +115,9 @@ export type CoreChannel<
112
115
  func?: ChannelDisconnect
113
116
  middleware?: PikkuMiddleware[]
114
117
  }
118
+ /** Handles any message that no `onMessageWiring` entry claimed. Without one, an unrouted message is dropped. */
115
119
  onMessage?: ChannelFunctionMessage
120
+ /** Routes messages by a key in their payload: the outer key is the field to switch on, the inner key its value. This is how one socket carries many operations. */
116
121
  onMessageWiring?: Record<
117
122
  string,
118
123
  Record<
@@ -125,17 +130,23 @@ export type CoreChannel<
125
130
  }
126
131
  >
127
132
  >
133
+ /** Wraps the connection: it runs on connect, where the session is established, not on every message. */
128
134
  middleware?: PikkuMiddleware[]
135
+ /** Wraps every message, which is where per-message concerns like rate limiting belong. */
129
136
  channelMiddleware?: Array<
130
137
  CorePikkuChannelMiddleware | CorePikkuChannelMiddlewareFactory
131
138
  >
139
+ /** Whether opening the channel requires a session. Defaults to true — a channel is closed unless it says otherwise. */
132
140
  auth?: boolean
141
+ /** Whether this channel carries binary frames. Text frames are still parsed as JSON. */
133
142
  binary?: boolean | null
143
+ /** Handles binary frames, which never reach `onMessage` because they are not JSON. Return a value to reply in kind. */
134
144
  onBinaryMessage?: (
135
145
  services: any,
136
146
  data: BinaryData,
137
147
  channel: PikkuChannel<ChannelData, any>
138
148
  ) => Promise<BinaryData | void> | BinaryData | void
149
+ /** Filters this channel in and out of a build — see the `tags` option on `pikku all`. It has no effect at runtime. */
139
150
  tags?: string[]
140
151
  }
141
152
 
@@ -174,6 +174,12 @@ type SplitBySpace<S extends string> = S extends `${infer First} ${infer Rest}`
174
174
  ? []
175
175
  : [S]
176
176
 
177
+ /**
178
+ * Strip the `...` a variadic positional carries, so `[files...]` validates
179
+ * against the `files` key the runtime parser fills.
180
+ */
181
+ type StripVariadic<S extends string> = S extends `${infer Name}...` ? Name : S
182
+
177
183
  /**
178
184
  * Recursively build a tuple by stripping brackets from each element
179
185
  */
@@ -182,7 +188,7 @@ type BuildParamsTuple<Parts extends readonly string[]> =
182
188
  infer First extends string,
183
189
  ...infer Rest extends string[],
184
190
  ]
185
- ? [StripBrackets<First>, ...BuildParamsTuple<Rest>]
191
+ ? [StripVariadic<StripBrackets<First>>, ...BuildParamsTuple<Rest>]
186
192
  : []
187
193
 
188
194
  /**
@@ -227,16 +233,39 @@ export type CoreCLICommandConfig<
227
233
  >,
228
234
  Params extends string = string,
229
235
  > = {
236
+ /**
237
+ * Positional arguments, written the way `--help` prints them: `<env>` is
238
+ * required, `[region]` optional. Each name must be a key of the function's
239
+ * input schema, so a typo is a compile error rather than an argument that
240
+ * silently never arrives.
241
+ */
230
242
  parameters?: ValidateParameters<Params, ExtractFunctionInput<FuncConfig>>
243
+ /** The function to run. Omit it on a command that exists only to hold `subcommands`. */
231
244
  func?: FuncConfig
245
+ /** The heading shown above this command's own help. */
232
246
  title?: string
247
+ /** The one-line summary listed beside the command name. */
233
248
  description?: string
249
+ /**
250
+ * Prints the result. The function returns data and this is the only thing
251
+ * that writes to stdout, which is what lets one command serve both a human
252
+ * reading it and a script parsing it.
253
+ */
234
254
  render?: PikkuCLIRender
255
+ /**
256
+ * Flags, keyed by the input field each one fills. A field with no entry is
257
+ * still accepted as `--field`; an entry is how it gets a short form, a
258
+ * default, or help text.
259
+ */
235
260
  options?: {
236
261
  [K in keyof ExtractFunctionInput<FuncConfig>]?: {
262
+ /** What this flag is for, as `--help` prints it. */
237
263
  description?: string
264
+ /** A single-letter alias, so `--verbose` can also be `-v`. */
238
265
  short?: string
266
+ /** How the argument is parsed. Inferred from the input schema when omitted. */
239
267
  type?: CLIOptionType
268
+ /** Used when the flag is absent, and shown in `--help` so nobody has to guess it. */
240
269
  default?: ExtractFunctionInput<FuncConfig>[K]
241
270
  /**
242
271
  * The values this option accepts. Rejected by the parser and listed in
@@ -246,13 +275,18 @@ export type CoreCLICommandConfig<
246
275
  choices?: ReadonlyArray<ExtractFunctionInput<FuncConfig>[K]>
247
276
  }
248
277
  }
278
+ /** Wraps every run of this command. */
249
279
  middleware?: PikkuMiddleware[]
280
+ /** Nested commands, keyed by the word that selects them: `deploy plan` is `deploy` with a `plan` subcommand. */
250
281
  subcommands?: Record<
251
282
  string,
252
283
  CoreCLICommandConfig<any, PikkuMiddleware, PikkuCLIRender, any>
253
284
  >
285
+ /** Whether running this requires a logged-in session. Defaults to true — a command is closed unless it says otherwise. */
254
286
  auth?: boolean
287
+ /** Permission checks run before the function, for a command that not every logged-in user may run. */
255
288
  permissions?: any[]
289
+ /** Runs when the parent is invoked with no subcommand named. */
256
290
  isDefault?: boolean
257
291
  }
258
292
 
@@ -308,14 +342,23 @@ export interface CoreCLI<
308
342
  PikkuMiddleware,
309
343
  PikkuCLIRender,
310
344
  > {
345
+ /** The command name a user types. It is also what `--help` prints as the program. */
311
346
  program: string
347
+ /** What the program is for, shown at the top of `--help`. */
312
348
  description?: string
349
+ /** Top-level commands, keyed by the word that selects them. */
313
350
  commands: Commands
351
+ /** Flags accepted before any command, for things that apply to the whole program. */
314
352
  options?: CLIOptions<Options>
353
+ /** Wraps every command. */
315
354
  middleware?: PikkuMiddleware[]
355
+ /** The fallback printer for commands that declare none of their own. */
316
356
  render?: PikkuCLIRender
357
+ /** A one-line description for listings, where the full `description` is too long. */
317
358
  summary?: string
359
+ /** Names of error classes any command may throw, so each one's registered exit behaviour is used. */
318
360
  errors?: string[]
361
+ /** Filters this program in and out of a build — see the `tags` option on `pikku all`. It has no effect at runtime. */
319
362
  tags?: string[]
320
363
  /**
321
364
  * Requires a session on the websocket serving this program remotely. Has no
@@ -1,6 +1,11 @@
1
1
  import type { CoreCLICommandConfig } from './cli.types.js'
2
2
 
3
- /** Identity at runtime; exists so a commands record can be typed and composed before `wireCLI`. */
3
+ /**
4
+ * Identity at runtime; exists so a commands record can be typed and composed
5
+ * before `wireCLI`.
6
+ *
7
+ * @example snippet: cliCommandContract
8
+ */
4
9
  export function defineCLICommands<
5
10
  T extends Record<string, CoreCLICommandConfig<any, any, any, any>>,
6
11
  >(commands: T): T {