esoul-sdk 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,43 @@
2
2
 
3
3
  Releases before 0.20.0 are recorded in the repository history only.
4
4
 
5
+ ## 0.24.0
6
+
7
+ ### Added
8
+
9
+ - `questions(ctx)` (esoul-sdk/server): a question your app needs the person to answer reaches the
10
+ platform's questions bell on every workspace — options as buttons, an Open button that lands on
11
+ the place inside your app (`useAppNav`), and the answer delivered to your own op as the person who
12
+ answered. `settle(key, …)` closes it when your own screen took the answer (docs/06-server.md,
13
+ "Asking the person").
14
+ - `mailWaitDirective(runCtx, …)`: an app tool pauses an agent network run until mail arrives at this
15
+ inbox on a thread or from a sender — the platform's own wait kernel (docs/04-tools.md, "Waiting
16
+ inside a network run").
17
+
18
+ ## 0.23.0
19
+
20
+ ### Added
21
+
22
+ - `useAppNav(nodeId, onTarget)` (esoul-sdk/react): the platform opens a place INSIDE your app —
23
+ the tasks pane lands on a conversation or a campaign, not just the app (docs/05-ui.md, "Opened
24
+ from outside").
25
+ - `origin: "server"` on an event definition: only the app's own server code (ops, routes, tasks,
26
+ webhooks) may append it; the browser's append and offline-queue doors and the PAT / SDK / MCP
27
+ dispatch routes refuse it (docs/03-events-and-state.md, "Events only the server emits").
28
+
29
+ ### Changed
30
+
31
+ - `readAppState(nodeId)` reads only for the call the platform is running: your app and its own
32
+ type in your workspace, another type only with a `workspaceTools` grant for it (refused with the
33
+ line to add), null for an app in another workspace, own type only from a webhook, and a throw
34
+ outside an op, route, task or webhook. `callWorkspaceTool` refuses a `pluginId`/`nodeId` that is
35
+ not the running call's own. The Forge box applies the same read rule.
36
+
37
+ ### Fixed
38
+
39
+ - `pluginServer.fileProducers` takes a producer with its own key type: `PluginFileProducer<MyKey>`
40
+ with `MyKey` declared as an `interface` no longer fails to typecheck against the map.
41
+
5
42
  ## 0.22.0
6
43
 
7
44
  ### Added
package/api-reference.md CHANGED
@@ -7,11 +7,11 @@ the complete list it teaches from. In a Forge workbench the source itself is rea
7
7
  Regenerate: `npm run docs:api`.
8
8
 
9
9
  ==============================================================================
10
- ## `esoul-sdk` — 250 exports
10
+ ## `esoul-sdk` — 252 exports
11
11
 
12
12
  Manifest, events, tools, ops, bindings, contracts, charts, access words — what app.tsx and the shared modules import.
13
13
 
14
- ### Functions and values (150)
14
+ ### Functions and values (151)
15
15
 
16
16
  #### `ASSET_APP_MAX_BYTES` — const · src/assets.ts
17
17
 
@@ -621,6 +621,14 @@ A gateway model id: `provider/model` ("deepseek/deepseek-v3.2").
621
621
  const LLM_MODEL_ID_RE: RegExp
622
622
  ```
623
623
 
624
+ #### `mailWaitDirective` — function · src/agent-wait.ts
625
+
626
+ The wait's event, for the toolkit's `eventCallback`. Throws on a malformed wait.
627
+
628
+ ```ts
629
+ function mailWaitDirective(runCtx: AgentRunToolContext, a: MailWaitArgs): Record<string, unknown>
630
+ ```
631
+
624
632
  #### `makeOpTool` — function · src/ops.ts
625
633
 
626
634
  `opTool`, bound to a caller. The package binds its own `callPluginOp`; the platform binds the one that carries the acting viewer and targets this deployment — same tool, different wire, decided once at the index.
@@ -1213,7 +1221,7 @@ A manifest with one entry removed (the bytes stay; they may be shared). Pure.
1213
1221
  function withoutAsset(assets: AssetsManifest | null | undefined, pluginId: string, name: string): AssetsManifest
1214
1222
  ```
1215
1223
 
1216
- ### Types (100)
1224
+ ### Types (101)
1217
1225
 
1218
1226
  #### `AgentRunToolContext` — interface · src/types.ts
1219
1227
 
@@ -1723,6 +1731,7 @@ interface EventDefinition<StateType extends ApplicationIdentifier> {
1723
1731
  triggerMeta?: TriggerMeta;
1724
1732
  sideEffect?: EventSideEffect;
1725
1733
  permission?: "user_exclusive";
1734
+ origin?: "server";
1726
1735
  conflictPolicy?: "mark" | "silent";
1727
1736
  conflictScope?: (eventData: any) => string[] | null | undefined;
1728
1737
  merge?: MergeDescription;
@@ -1975,6 +1984,25 @@ interface LabelShape {
1975
1984
  }
1976
1985
  ```
1977
1986
 
1987
+ #### `MailWaitArgs` — interface · src/agent-wait.ts
1988
+
1989
+ ```ts
1990
+ interface MailWaitArgs {
1991
+ workspaceId: string;
1992
+ inboxNodeId: string;
1993
+ inboxName: string;
1994
+ toolName: string;
1995
+ threadId?: string;
1996
+ fromEmail?: string;
1997
+ subject?: string;
1998
+ afterTimestamp: number;
1999
+ timeoutSeconds?: number;
2000
+ directiveId: string;
2001
+ onResume?: string;
2002
+ onTimeout?: string;
2003
+ }
2004
+ ```
2005
+
1978
2006
  #### `MergeDescription` — interface · src/types.ts
1979
2007
 
1980
2008
  ```ts
@@ -2393,11 +2421,11 @@ interface UsesDecl {
2393
2421
  ```
2394
2422
 
2395
2423
  ==============================================================================
2396
- ## `esoul-sdk/server` — 112 exports
2424
+ ## `esoul-sdk/server` — 115 exports
2397
2425
 
2398
2426
  Server code only (server.ts, ops, routes, tasks): the viewer, the app's database, files, connections, machines, charts, route tokens.
2399
2427
 
2400
- ### Functions and values (35)
2428
+ ### Functions and values (36)
2401
2429
 
2402
2430
  #### `APPROVAL_WAIT` — const · src/computer.ts
2403
2431
 
@@ -2631,9 +2659,17 @@ The consent-enforcing files API for plugin server code. Every call re-checks you
2631
2659
  function pluginFiles(_ctx: PluginFilesCtx): Promise<FilesApi>
2632
2660
  ```
2633
2661
 
2662
+ #### `questions` — function · src/server.ts
2663
+
2664
+ THE PLATFORM'S QUESTIONS BELL. A question an app needs the person to answer (an agent waiting on a decision) reaches them wherever they are — the bell, on every workspace — not only when they open the app:
2665
+
2666
+ ```ts
2667
+ function questions(_ctx: { pluginId: string; workspaceId: string; nodeId: string; viewer?: { userId: string | null } }): Questions
2668
+ ```
2669
+
2634
2670
  #### `readAppState` — function · src/server.ts
2635
2671
 
2636
- Read one app's state folded to head, by nodeId — server truth for an op or a task. Null when no such app exists. HOST-ONLY.
2672
+ Read one app's state folded to head, by nodeId — server truth for an op, route, task or webhook. It reads for the app instance of the call the platform is running: your own app and other instances of its type in your workspace; another app type only when plugin.json `workspaceTools` grants a tool of that type (the refusal names the line to add). Null when no such app exists — an app in another workspace is null too. A webhook (it runs for no instance) finds only instances of its own type. Outside a running call it throws. HOST-ONLY.
2637
2673
 
2638
2674
  ```ts
2639
2675
  function readAppState( _nodeId: string, ): Promise<{ nodeId: string; workspaceId: string; applicationType: string; foldedSeq: number; state: Record<string, unknown> } | null>
@@ -2679,7 +2715,23 @@ WHO IS THIS, IN WORDS. The caller's own name and email, for an app with a reason
2679
2715
  function viewerProfile(_viewer: PluginViewer): Promise<ViewerProfile | null>
2680
2716
  ```
2681
2717
 
2682
- ### Types (77)
2718
+ ### Types (79)
2719
+
2720
+ #### `AppQuestion` — interface · src/server.ts
2721
+
2722
+ A question for the person (questions(ctx).ask).
2723
+
2724
+ ```ts
2725
+ interface AppQuestion {
2726
+ key: string;
2727
+ question: string;
2728
+ options?: string[];
2729
+ details?: Record<string, unknown>;
2730
+ open?: { kind: string; key: string; label?: string };
2731
+ answer: { op: string; args?: Record<string, unknown> };
2732
+ expiresInSeconds?: number;
2733
+ }
2734
+ ```
2683
2735
 
2684
2736
  #### `AppRolePerson` — interface · src/server.ts
2685
2737
 
@@ -3387,7 +3439,7 @@ interface PluginServerModule {
3387
3439
  webhooks?: Record<string, PluginWebhookHandler>;
3388
3440
  ops?: Record<string, PluginOpHandler>;
3389
3441
  routes?: Record<string, PluginRouteHandler>;
3390
- fileProducers?: Record<string, PluginFileProducer>;
3442
+ fileProducers?: Record<string, PluginFileProducer<any>>;
3391
3443
  }
3392
3444
  ```
3393
3445
 
@@ -3478,6 +3530,15 @@ interface ProgramStepState {
3478
3530
  }
3479
3531
  ```
3480
3532
 
3533
+ #### `Questions` — interface · src/server.ts
3534
+
3535
+ ```ts
3536
+ interface Questions {
3537
+ ask(q: AppQuestion): Promise<{ questionId: string; created: boolean }>;
3538
+ settle(key: string, outcome: { answer: string } | { cancelled: true }): Promise<void>;
3539
+ }
3540
+ ```
3541
+
3481
3542
  #### `RouteTokenGrant` — interface · src/server.ts
3482
3543
 
3483
3544
  ```ts
@@ -3689,11 +3750,11 @@ type WorkspaceGrant = { scope: "folder"; root: string } | { scope: "computer" };
3689
3750
  ```
3690
3751
 
3691
3752
  ==============================================================================
3692
- ## `esoul-sdk/react` — 69 exports
3753
+ ## `esoul-sdk/react` — 71 exports
3693
3754
 
3694
3755
  The app's UI: hooks for the viewer, the app's state, realtime, workspace files and tools.
3695
3756
 
3696
- ### Functions and values (36)
3757
+ ### Functions and values (37)
3697
3758
 
3698
3759
  #### `ConnectAccount` — function · src/react.ts
3699
3760
 
@@ -3815,6 +3876,14 @@ True when the current viewer may mutate this workspace.
3815
3876
  function useAppCanEdit(): boolean
3816
3877
  ```
3817
3878
 
3879
+ #### `useAppNav` — function · src/react.ts
3880
+
3881
+ "Open THIS inside the app": the platform's tasks pane, a link chip or a recall hit asks for a place inside this app instance (the keys are the app's own — say which in your tool and task descriptions). `onTarget` runs with the target waiting when the app mounts, and with each one asked for while it is on screen. Open it and select it; an unknown key does nothing.
3882
+
3883
+ ```ts
3884
+ function useAppNav(_nodeId: string, _onTarget: (target: AppNavTarget) => void): void
3885
+ ```
3886
+
3818
3887
  #### `useCredential` — function · src/react.ts
3819
3888
 
3820
3889
  The state of one of your plugin.json `credentials` slots for the person looking at the app.
@@ -3983,7 +4052,15 @@ The account's computers as a panel: online, the apps each serves, Disconnect.
3983
4052
  function YourComputers(_props: { className?: string }): any
3984
4053
  ```
3985
4054
 
3986
- ### Types (33)
4055
+ ### Types (34)
4056
+
4057
+ #### `AppNavTarget` — type · src/react.ts
4058
+
4059
+ A place inside an app, as named by whoever asks to open it: `{ thread: "…" }`, `{ campaign: "…" }`.
4060
+
4061
+ ```ts
4062
+ type AppNavTarget = Record<string, string>;
4063
+ ```
3987
4064
 
3988
4065
  #### `DeviceRunStarted` — interface · src/device.ts
3989
4066
 
@@ -0,0 +1,47 @@
1
+ /**
2
+ * A DURABLE MAIL WAIT from an app's tool inside an agent network run: the run
3
+ * pauses — minutes or days, nothing running — until mail arrives at this inbox
4
+ * on a thread or from a sender, then resumes with the arrival in its history.
5
+ *
6
+ * The platform's agent runtime owns the wait (the same kernel `wait_for_reply`
7
+ * uses): it parks on `workspace/email.message_arrived`, checks the inbox's
8
+ * folded `messages` for an arrival newer than `afterTimestamp` before and
9
+ * after parking, and resumes with `onResume` (its `{{messageId}}`, `{{from}}`,
10
+ * `{{subject}}`, `{{threadId}}` filled in) or `onTimeout`.
11
+ *
12
+ * toolkitCreator: (identifier, _chat, eventCallback, _msg, runCtx) => ({
13
+ * [`wait_for_email_${base}`]: { …, execute: async (args) => {
14
+ * if (!runCtx) return "Only inside an agent network run.";
15
+ * eventCallback(mailWaitDirective(runCtx, { workspaceId, inboxNodeId, inboxName, toolName, threadId, afterTimestamp, directiveId }));
16
+ * runCtx.setWaitDirectiveCreatedThisStep();
17
+ * return "Waiting…";
18
+ * } },
19
+ * })
20
+ *
21
+ * The inbox's state must keep a `messages` map (`{ id, threadId, labelIds,
22
+ * from, subject, internalDate }`) and its app must announce arrivals as that
23
+ * event (the platform does it for the SDK Gmail app). Pure.
24
+ */
25
+ import type { AgentRunToolContext } from "./types.js";
26
+ export interface MailWaitArgs {
27
+ workspaceId: string;
28
+ /** The inbox app instance (the wait is scoped to it). */
29
+ inboxNodeId: string;
30
+ inboxName: string;
31
+ /** The tool's name as the model sees it (for the run's log). */
32
+ toolName: string;
33
+ /** Exactly one of: the next message on this thread, or from this sender (optionally with this subject). */
34
+ threadId?: string;
35
+ fromEmail?: string;
36
+ subject?: string;
37
+ /** Only mail newer than this counts (epoch ms) — the newest message the agent has already seen. */
38
+ afterTimestamp: number;
39
+ /** Default 7 days. */
40
+ timeoutSeconds?: number;
41
+ /** Unique per wait (the caller's id — keep it stable across a retried tool call). */
42
+ directiveId: string;
43
+ onResume?: string;
44
+ onTimeout?: string;
45
+ }
46
+ /** The wait's event, for the toolkit's `eventCallback`. Throws on a malformed wait. */
47
+ export declare function mailWaitDirective(runCtx: AgentRunToolContext, a: MailWaitArgs): Record<string, unknown>;
@@ -0,0 +1,47 @@
1
+ const esc = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
2
+ /** The wait's event, for the toolkit's `eventCallback`. Throws on a malformed wait. */
3
+ export function mailWaitDirective(runCtx, a) {
4
+ const threadId = a.threadId?.trim() || undefined;
5
+ const fromEmail = a.fromEmail?.trim() || undefined;
6
+ const subject = a.subject?.trim() || undefined;
7
+ if (!!threadId === !!fromEmail)
8
+ throw new Error("mailWaitDirective: give threadId OR fromEmail.");
9
+ const timeoutSeconds = Math.max(60, Math.min(30 * 86_400, Math.round(a.timeoutSeconds ?? 7 * 86_400)));
10
+ const scope = `async.data.workspaceId == "${esc(a.workspaceId)}" && async.data.nodeId == "${esc(a.inboxNodeId)}" && `;
11
+ const matchExpression = threadId
12
+ ? `${scope}async.data.threadId == "${esc(threadId)}" && !async.data.labelIds.exists(l, l == "SENT")`
13
+ : `${scope}async.data.fromEmail == "${esc(fromEmail.toLowerCase())}" && ` +
14
+ (subject ? `async.data.subject == "${esc(subject)}" && ` : "") +
15
+ `!async.data.labelIds.exists(l, l == "SENT")`;
16
+ const what = threadId ? `thread ${threadId}` : `sender ${fromEmail}${subject ? ` subject "${subject}"` : ""}`;
17
+ return {
18
+ eventName: "agent_wait_directive_created",
19
+ eventData: {
20
+ runId: runCtx.agentRunId,
21
+ directiveId: a.directiveId,
22
+ agentNodeId: runCtx.currentAgentNodeId ?? null,
23
+ toolName: a.toolName,
24
+ eventName: "workspace/email.message_arrived",
25
+ matchExpression,
26
+ timeoutSeconds,
27
+ templates: {
28
+ onResume: a.onResume ?? `[Email landed in "${a.inboxName}" (${what}): messageId={{messageId}}, from={{from}}, subject="{{subject}}", threadId={{threadId}}.]`,
29
+ onTimeout: a.onTimeout ?? `[No email in "${a.inboxName}" (${what}) within ${timeoutSeconds}s.]`,
30
+ },
31
+ reason: `Waiting for email in ${a.inboxName} (${what})`,
32
+ prewaitCheck: {
33
+ type: "workspace_email_arrival",
34
+ workspaceId: a.workspaceId,
35
+ nodeId: a.inboxNodeId,
36
+ ...(threadId ? { threadId } : { fromEmail }),
37
+ ...(subject && !threadId ? { subject } : {}),
38
+ afterTimestamp: a.afterTimestamp,
39
+ },
40
+ },
41
+ timestamp: Date.now(),
42
+ workspaceId: a.workspaceId,
43
+ applicationId: runCtx.agentBuilderNodeId,
44
+ instanceName: "",
45
+ chatIdSource: runCtx.agentRunId,
46
+ };
47
+ }
package/dist/index.d.ts CHANGED
@@ -25,6 +25,7 @@ export * from "./chart.js";
25
25
  * the decision and the merges (records by id, fields, lines). The hook is `useRemoteReconcile` in esoul-sdk/react.
26
26
  */
27
27
  export * from "./editor-sync.js";
28
+ export * from "./agent-wait.js";
28
29
  /**
29
30
  * The agent tool for one op, declared once: `opTool(ops.play, { say })` in app.tsx gives a tool whose
30
31
  * parameters ARE the op's zod input and whose call goes to `callPluginOp` — as the acting viewer on
package/dist/index.js CHANGED
@@ -25,6 +25,7 @@ export * from "./chart.js";
25
25
  * the decision and the merges (records by id, fields, lines). The hook is `useRemoteReconcile` in esoul-sdk/react.
26
26
  */
27
27
  export * from "./editor-sync.js";
28
+ export * from "./agent-wait.js";
28
29
  import { callPluginOp } from "./helpers.js";
29
30
  import { makeOpTool } from "./ops.js";
30
31
  /**
@@ -62,8 +62,8 @@ export declare const PluginConnectionSchema: z.ZodEffects<z.ZodObject<{
62
62
  /** apiKey: header NAMES the plugin will send (never values). */
63
63
  headerNames: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
64
64
  }, "strict", z.ZodTypeAny, {
65
- label: string;
66
65
  kind: "oauth2" | "apiKey";
66
+ label: string;
67
67
  key: string;
68
68
  description?: string | undefined;
69
69
  authorizeUrl?: string | undefined;
@@ -73,8 +73,8 @@ export declare const PluginConnectionSchema: z.ZodEffects<z.ZodObject<{
73
73
  clientSecretEnv?: string | undefined;
74
74
  headerNames?: string[] | undefined;
75
75
  }, {
76
- label: string;
77
76
  kind: "oauth2" | "apiKey";
77
+ label: string;
78
78
  key: string;
79
79
  description?: string | undefined;
80
80
  authorizeUrl?: string | undefined;
@@ -84,8 +84,8 @@ export declare const PluginConnectionSchema: z.ZodEffects<z.ZodObject<{
84
84
  clientSecretEnv?: string | undefined;
85
85
  headerNames?: string[] | undefined;
86
86
  }>, {
87
- label: string;
88
87
  kind: "oauth2" | "apiKey";
88
+ label: string;
89
89
  key: string;
90
90
  description?: string | undefined;
91
91
  authorizeUrl?: string | undefined;
@@ -95,8 +95,8 @@ export declare const PluginConnectionSchema: z.ZodEffects<z.ZodObject<{
95
95
  clientSecretEnv?: string | undefined;
96
96
  headerNames?: string[] | undefined;
97
97
  }, {
98
- label: string;
99
98
  kind: "oauth2" | "apiKey";
99
+ label: string;
100
100
  key: string;
101
101
  description?: string | undefined;
102
102
  authorizeUrl?: string | undefined;
@@ -809,12 +809,12 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
809
809
  values: z.ZodArray<z.ZodString, "many">;
810
810
  describe: z.ZodOptional<z.ZodString>;
811
811
  }, "strict", z.ZodTypeAny, {
812
- type: "enum";
813
812
  values: string[];
813
+ type: "enum";
814
814
  describe?: string | undefined;
815
815
  }, {
816
- type: "enum";
817
816
  values: string[];
817
+ type: "enum";
818
818
  describe?: string | undefined;
819
819
  }>, z.ZodObject<{
820
820
  type: z.ZodLiteral<"bool">;
@@ -856,7 +856,6 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
856
856
  argv: string[];
857
857
  timeoutSeconds?: number | undefined;
858
858
  cwd?: string | undefined;
859
- result?: "json" | "text" | undefined;
860
859
  params?: Record<string, "string" | "number" | "int" | "bool" | {
861
860
  type: "number" | "int";
862
861
  min?: number | undefined;
@@ -869,13 +868,14 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
869
868
  allowDash?: boolean | undefined;
870
869
  describe?: string | undefined;
871
870
  } | {
872
- type: "enum";
873
871
  values: string[];
872
+ type: "enum";
874
873
  describe?: string | undefined;
875
874
  } | {
876
875
  type: "bool";
877
876
  describe?: string | undefined;
878
877
  }> | undefined;
878
+ result?: "text" | "json" | undefined;
879
879
  config?: string[] | undefined;
880
880
  describe?: string | undefined;
881
881
  env?: Record<string, string> | undefined;
@@ -891,7 +891,6 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
891
891
  argv: string[];
892
892
  timeoutSeconds?: number | undefined;
893
893
  cwd?: string | undefined;
894
- result?: "json" | "text" | undefined;
895
894
  params?: Record<string, "string" | "number" | "int" | "bool" | {
896
895
  type: "number" | "int";
897
896
  min?: number | undefined;
@@ -904,13 +903,14 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
904
903
  allowDash?: boolean | undefined;
905
904
  describe?: string | undefined;
906
905
  } | {
907
- type: "enum";
908
906
  values: string[];
907
+ type: "enum";
909
908
  describe?: string | undefined;
910
909
  } | {
911
910
  type: "bool";
912
911
  describe?: string | undefined;
913
912
  }> | undefined;
913
+ result?: "text" | "json" | undefined;
914
914
  config?: string[] | undefined;
915
915
  describe?: string | undefined;
916
916
  env?: Record<string, string> | undefined;
@@ -1004,7 +1004,6 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
1004
1004
  argv: string[];
1005
1005
  timeoutSeconds?: number | undefined;
1006
1006
  cwd?: string | undefined;
1007
- result?: "json" | "text" | undefined;
1008
1007
  params?: Record<string, "string" | "number" | "int" | "bool" | {
1009
1008
  type: "number" | "int";
1010
1009
  min?: number | undefined;
@@ -1017,13 +1016,14 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
1017
1016
  allowDash?: boolean | undefined;
1018
1017
  describe?: string | undefined;
1019
1018
  } | {
1020
- type: "enum";
1021
1019
  values: string[];
1020
+ type: "enum";
1022
1021
  describe?: string | undefined;
1023
1022
  } | {
1024
1023
  type: "bool";
1025
1024
  describe?: string | undefined;
1026
1025
  }> | undefined;
1026
+ result?: "text" | "json" | undefined;
1027
1027
  config?: string[] | undefined;
1028
1028
  describe?: string | undefined;
1029
1029
  env?: Record<string, string> | undefined;
@@ -1063,7 +1063,6 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
1063
1063
  argv: string[];
1064
1064
  timeoutSeconds?: number | undefined;
1065
1065
  cwd?: string | undefined;
1066
- result?: "json" | "text" | undefined;
1067
1066
  params?: Record<string, "string" | "number" | "int" | "bool" | {
1068
1067
  type: "number" | "int";
1069
1068
  min?: number | undefined;
@@ -1076,13 +1075,14 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
1076
1075
  allowDash?: boolean | undefined;
1077
1076
  describe?: string | undefined;
1078
1077
  } | {
1079
- type: "enum";
1080
1078
  values: string[];
1079
+ type: "enum";
1081
1080
  describe?: string | undefined;
1082
1081
  } | {
1083
1082
  type: "bool";
1084
1083
  describe?: string | undefined;
1085
1084
  }> | undefined;
1085
+ result?: "text" | "json" | undefined;
1086
1086
  config?: string[] | undefined;
1087
1087
  describe?: string | undefined;
1088
1088
  env?: Record<string, string> | undefined;
@@ -1122,7 +1122,6 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
1122
1122
  argv: string[];
1123
1123
  timeoutSeconds?: number | undefined;
1124
1124
  cwd?: string | undefined;
1125
- result?: "json" | "text" | undefined;
1126
1125
  params?: Record<string, "string" | "number" | "int" | "bool" | {
1127
1126
  type: "number" | "int";
1128
1127
  min?: number | undefined;
@@ -1135,13 +1134,14 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
1135
1134
  allowDash?: boolean | undefined;
1136
1135
  describe?: string | undefined;
1137
1136
  } | {
1138
- type: "enum";
1139
1137
  values: string[];
1138
+ type: "enum";
1140
1139
  describe?: string | undefined;
1141
1140
  } | {
1142
1141
  type: "bool";
1143
1142
  describe?: string | undefined;
1144
1143
  }> | undefined;
1144
+ result?: "text" | "json" | undefined;
1145
1145
  config?: string[] | undefined;
1146
1146
  describe?: string | undefined;
1147
1147
  env?: Record<string, string> | undefined;
@@ -1181,7 +1181,6 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
1181
1181
  argv: string[];
1182
1182
  timeoutSeconds?: number | undefined;
1183
1183
  cwd?: string | undefined;
1184
- result?: "json" | "text" | undefined;
1185
1184
  params?: Record<string, "string" | "number" | "int" | "bool" | {
1186
1185
  type: "number" | "int";
1187
1186
  min?: number | undefined;
@@ -1194,13 +1193,14 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
1194
1193
  allowDash?: boolean | undefined;
1195
1194
  describe?: string | undefined;
1196
1195
  } | {
1197
- type: "enum";
1198
1196
  values: string[];
1197
+ type: "enum";
1199
1198
  describe?: string | undefined;
1200
1199
  } | {
1201
1200
  type: "bool";
1202
1201
  describe?: string | undefined;
1203
1202
  }> | undefined;
1203
+ result?: "text" | "json" | undefined;
1204
1204
  config?: string[] | undefined;
1205
1205
  describe?: string | undefined;
1206
1206
  env?: Record<string, string> | undefined;
@@ -1255,8 +1255,8 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
1255
1255
  /** apiKey: header NAMES the plugin will send (never values). */
1256
1256
  headerNames: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
1257
1257
  }, "strict", z.ZodTypeAny, {
1258
- label: string;
1259
1258
  kind: "oauth2" | "apiKey";
1259
+ label: string;
1260
1260
  key: string;
1261
1261
  description?: string | undefined;
1262
1262
  authorizeUrl?: string | undefined;
@@ -1266,8 +1266,8 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
1266
1266
  clientSecretEnv?: string | undefined;
1267
1267
  headerNames?: string[] | undefined;
1268
1268
  }, {
1269
- label: string;
1270
1269
  kind: "oauth2" | "apiKey";
1270
+ label: string;
1271
1271
  key: string;
1272
1272
  description?: string | undefined;
1273
1273
  authorizeUrl?: string | undefined;
@@ -1277,8 +1277,8 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
1277
1277
  clientSecretEnv?: string | undefined;
1278
1278
  headerNames?: string[] | undefined;
1279
1279
  }>, {
1280
- label: string;
1281
1280
  kind: "oauth2" | "apiKey";
1281
+ label: string;
1282
1282
  key: string;
1283
1283
  description?: string | undefined;
1284
1284
  authorizeUrl?: string | undefined;
@@ -1288,8 +1288,8 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
1288
1288
  clientSecretEnv?: string | undefined;
1289
1289
  headerNames?: string[] | undefined;
1290
1290
  }, {
1291
- label: string;
1292
1291
  kind: "oauth2" | "apiKey";
1292
+ label: string;
1293
1293
  key: string;
1294
1294
  description?: string | undefined;
1295
1295
  authorizeUrl?: string | undefined;
@@ -1453,13 +1453,13 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
1453
1453
  events: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
1454
1454
  models: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
1455
1455
  }, "strict", z.ZodTypeAny, {
1456
+ models?: string[] | undefined;
1456
1457
  tools?: string[] | undefined;
1457
1458
  events?: string[] | undefined;
1458
- models?: string[] | undefined;
1459
1459
  }, {
1460
+ models?: string[] | undefined;
1460
1461
  tools?: string[] | undefined;
1461
1462
  events?: string[] | undefined;
1462
- models?: string[] | undefined;
1463
1463
  }>>>;
1464
1464
  /**
1465
1465
  * The app's realtime TOPICS, and who hears each (forge-sdk-spec-and-tests
@@ -1552,9 +1552,9 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
1552
1552
  }>>>;
1553
1553
  }, "strict", z.ZodTypeAny, {
1554
1554
  name: string;
1555
+ id: string;
1555
1556
  version: string;
1556
1557
  applicationType: string;
1557
- id: string;
1558
1558
  description: string;
1559
1559
  scopes: string[];
1560
1560
  ops: string[] | Record<string, {
@@ -1578,8 +1578,8 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
1578
1578
  }[];
1579
1579
  workspaceTools: string[];
1580
1580
  connections: {
1581
- label: string;
1582
1581
  kind: "oauth2" | "apiKey";
1582
+ label: string;
1583
1583
  key: string;
1584
1584
  description?: string | undefined;
1585
1585
  authorizeUrl?: string | undefined;
@@ -1603,7 +1603,6 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
1603
1603
  argv: string[];
1604
1604
  timeoutSeconds?: number | undefined;
1605
1605
  cwd?: string | undefined;
1606
- result?: "json" | "text" | undefined;
1607
1606
  params?: Record<string, "string" | "number" | "int" | "bool" | {
1608
1607
  type: "number" | "int";
1609
1608
  min?: number | undefined;
@@ -1616,13 +1615,14 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
1616
1615
  allowDash?: boolean | undefined;
1617
1616
  describe?: string | undefined;
1618
1617
  } | {
1619
- type: "enum";
1620
1618
  values: string[];
1619
+ type: "enum";
1621
1620
  describe?: string | undefined;
1622
1621
  } | {
1623
1622
  type: "bool";
1624
1623
  describe?: string | undefined;
1625
1624
  }> | undefined;
1625
+ result?: "text" | "json" | undefined;
1626
1626
  config?: string[] | undefined;
1627
1627
  describe?: string | undefined;
1628
1628
  env?: Record<string, string> | undefined;
@@ -1716,9 +1716,9 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
1716
1716
  optional?: boolean | undefined;
1717
1717
  }> | undefined;
1718
1718
  provides?: Record<string, {
1719
+ models?: string[] | undefined;
1719
1720
  tools?: string[] | undefined;
1720
1721
  events?: string[] | undefined;
1721
- models?: string[] | undefined;
1722
1722
  }> | undefined;
1723
1723
  channel?: {
1724
1724
  topics: Record<string, {
@@ -1741,9 +1741,9 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
1741
1741
  }> | undefined;
1742
1742
  }, {
1743
1743
  name: string;
1744
+ id: string;
1744
1745
  version: string;
1745
1746
  applicationType: string;
1746
- id: string;
1747
1747
  description: string;
1748
1748
  manifestVersion: 1;
1749
1749
  entry: string;
@@ -1752,7 +1752,6 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
1752
1752
  argv: string[];
1753
1753
  timeoutSeconds?: number | undefined;
1754
1754
  cwd?: string | undefined;
1755
- result?: "json" | "text" | undefined;
1756
1755
  params?: Record<string, "string" | "number" | "int" | "bool" | {
1757
1756
  type: "number" | "int";
1758
1757
  min?: number | undefined;
@@ -1765,13 +1764,14 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
1765
1764
  allowDash?: boolean | undefined;
1766
1765
  describe?: string | undefined;
1767
1766
  } | {
1768
- type: "enum";
1769
1767
  values: string[];
1768
+ type: "enum";
1770
1769
  describe?: string | undefined;
1771
1770
  } | {
1772
1771
  type: "bool";
1773
1772
  describe?: string | undefined;
1774
1773
  }> | undefined;
1774
+ result?: "text" | "json" | undefined;
1775
1775
  config?: string[] | undefined;
1776
1776
  describe?: string | undefined;
1777
1777
  env?: Record<string, string> | undefined;
@@ -1874,8 +1874,8 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
1874
1874
  } | undefined;
1875
1875
  workspaceTools?: string[] | undefined;
1876
1876
  connections?: {
1877
- label: string;
1878
1877
  kind: "oauth2" | "apiKey";
1878
+ label: string;
1879
1879
  key: string;
1880
1880
  description?: string | undefined;
1881
1881
  authorizeUrl?: string | undefined;
@@ -1905,9 +1905,9 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
1905
1905
  optional?: boolean | undefined;
1906
1906
  }> | undefined;
1907
1907
  provides?: Record<string, {
1908
+ models?: string[] | undefined;
1908
1909
  tools?: string[] | undefined;
1909
1910
  events?: string[] | undefined;
1910
- models?: string[] | undefined;
1911
1911
  }> | undefined;
1912
1912
  channel?: {
1913
1913
  topics: Record<string, {
@@ -1930,9 +1930,9 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
1930
1930
  }> | undefined;
1931
1931
  }>, {
1932
1932
  name: string;
1933
+ id: string;
1933
1934
  version: string;
1934
1935
  applicationType: string;
1935
- id: string;
1936
1936
  description: string;
1937
1937
  scopes: string[];
1938
1938
  ops: string[] | Record<string, {
@@ -1956,8 +1956,8 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
1956
1956
  }[];
1957
1957
  workspaceTools: string[];
1958
1958
  connections: {
1959
- label: string;
1960
1959
  kind: "oauth2" | "apiKey";
1960
+ label: string;
1961
1961
  key: string;
1962
1962
  description?: string | undefined;
1963
1963
  authorizeUrl?: string | undefined;
@@ -1981,7 +1981,6 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
1981
1981
  argv: string[];
1982
1982
  timeoutSeconds?: number | undefined;
1983
1983
  cwd?: string | undefined;
1984
- result?: "json" | "text" | undefined;
1985
1984
  params?: Record<string, "string" | "number" | "int" | "bool" | {
1986
1985
  type: "number" | "int";
1987
1986
  min?: number | undefined;
@@ -1994,13 +1993,14 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
1994
1993
  allowDash?: boolean | undefined;
1995
1994
  describe?: string | undefined;
1996
1995
  } | {
1997
- type: "enum";
1998
1996
  values: string[];
1997
+ type: "enum";
1999
1998
  describe?: string | undefined;
2000
1999
  } | {
2001
2000
  type: "bool";
2002
2001
  describe?: string | undefined;
2003
2002
  }> | undefined;
2003
+ result?: "text" | "json" | undefined;
2004
2004
  config?: string[] | undefined;
2005
2005
  describe?: string | undefined;
2006
2006
  env?: Record<string, string> | undefined;
@@ -2094,9 +2094,9 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
2094
2094
  optional?: boolean | undefined;
2095
2095
  }> | undefined;
2096
2096
  provides?: Record<string, {
2097
+ models?: string[] | undefined;
2097
2098
  tools?: string[] | undefined;
2098
2099
  events?: string[] | undefined;
2099
- models?: string[] | undefined;
2100
2100
  }> | undefined;
2101
2101
  channel?: {
2102
2102
  topics: Record<string, {
@@ -2119,9 +2119,9 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
2119
2119
  }> | undefined;
2120
2120
  }, {
2121
2121
  name: string;
2122
+ id: string;
2122
2123
  version: string;
2123
2124
  applicationType: string;
2124
- id: string;
2125
2125
  description: string;
2126
2126
  manifestVersion: 1;
2127
2127
  entry: string;
@@ -2130,7 +2130,6 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
2130
2130
  argv: string[];
2131
2131
  timeoutSeconds?: number | undefined;
2132
2132
  cwd?: string | undefined;
2133
- result?: "json" | "text" | undefined;
2134
2133
  params?: Record<string, "string" | "number" | "int" | "bool" | {
2135
2134
  type: "number" | "int";
2136
2135
  min?: number | undefined;
@@ -2143,13 +2142,14 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
2143
2142
  allowDash?: boolean | undefined;
2144
2143
  describe?: string | undefined;
2145
2144
  } | {
2146
- type: "enum";
2147
2145
  values: string[];
2146
+ type: "enum";
2148
2147
  describe?: string | undefined;
2149
2148
  } | {
2150
2149
  type: "bool";
2151
2150
  describe?: string | undefined;
2152
2151
  }> | undefined;
2152
+ result?: "text" | "json" | undefined;
2153
2153
  config?: string[] | undefined;
2154
2154
  describe?: string | undefined;
2155
2155
  env?: Record<string, string> | undefined;
@@ -2252,8 +2252,8 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
2252
2252
  } | undefined;
2253
2253
  workspaceTools?: string[] | undefined;
2254
2254
  connections?: {
2255
- label: string;
2256
2255
  kind: "oauth2" | "apiKey";
2256
+ label: string;
2257
2257
  key: string;
2258
2258
  description?: string | undefined;
2259
2259
  authorizeUrl?: string | undefined;
@@ -2283,9 +2283,9 @@ export declare const PluginManifestSchema: z.ZodEffects<z.ZodObject<{
2283
2283
  optional?: boolean | undefined;
2284
2284
  }> | undefined;
2285
2285
  provides?: Record<string, {
2286
+ models?: string[] | undefined;
2286
2287
  tools?: string[] | undefined;
2287
2288
  events?: string[] | undefined;
2288
- models?: string[] | undefined;
2289
2289
  }> | undefined;
2290
2290
  channel?: {
2291
2291
  topics: Record<string, {
package/dist/react.d.ts CHANGED
@@ -67,6 +67,15 @@ export interface WorkspaceNav {
67
67
  * already on.
68
68
  */
69
69
  export declare function useWorkspaceNav(): WorkspaceNav;
70
+ /** A place inside an app, as named by whoever asks to open it: `{ thread: "…" }`, `{ campaign: "…" }`. */
71
+ export type AppNavTarget = Record<string, string>;
72
+ /**
73
+ * "Open THIS inside the app": the platform's tasks pane, a link chip or a recall hit asks for a
74
+ * place inside this app instance (the keys are the app's own — say which in your tool and task
75
+ * descriptions). `onTarget` runs with the target waiting when the app mounts, and with each one
76
+ * asked for while it is on screen. Open it and select it; an unknown key does nothing.
77
+ */
78
+ export declare function useAppNav(_nodeId: string, _onTarget: (target: AppNavTarget) => void): void;
70
79
  import type { FileEntry, FileListOptions, FileRef, FileSource } from "./files.js";
71
80
  import type { TransferSnapshot } from "./transfers.js";
72
81
  import type { LabelPoint, LabelShape } from "./labelme.js";
package/dist/react.js CHANGED
@@ -33,6 +33,15 @@ export function useWorkspaceTools(_identity) {
33
33
  export function useWorkspaceNav() {
34
34
  return hostOnly("useWorkspaceNav");
35
35
  }
36
+ /**
37
+ * "Open THIS inside the app": the platform's tasks pane, a link chip or a recall hit asks for a
38
+ * place inside this app instance (the keys are the app's own — say which in your tool and task
39
+ * descriptions). `onTarget` runs with the target waiting when the app mounts, and with each one
40
+ * asked for while it is on screen. Open it and select it; an unknown key does nothing.
41
+ */
42
+ export function useAppNav(_nodeId, _onTarget) {
43
+ return hostOnly("useAppNav");
44
+ }
36
45
  /** A files call from the UI refused — `kind` is the FileSourceErrorKind when the source said why. */
37
46
  export class FilesBrowseError extends Error {
38
47
  kind;
package/dist/server.d.ts CHANGED
@@ -208,8 +208,12 @@ export interface PluginServerModule {
208
208
  ops?: Record<string, PluginOpHandler>;
209
209
  /** routeName → handler, for names declared in plugin.json `routes`. */
210
210
  routes?: Record<string, PluginRouteHandler>;
211
- /** key → producer, for keys declared in plugin.json `fileProducers`: the app's own bytes as a transfer source. */
212
- fileProducers?: Record<string, PluginFileProducer>;
211
+ /**
212
+ * key → producer, for keys declared in plugin.json `fileProducers`: the app's own bytes as a
213
+ * transfer source. Each producer keeps its own key type (`PluginFileProducer<MyKey>`, an
214
+ * interface or a type) — the platform hands `open` the key the transfer item named.
215
+ */
216
+ fileProducers?: Record<string, PluginFileProducer<any>>;
213
217
  }
214
218
  export type PluginConnectionCredentials = {
215
219
  kind: "oauth2";
@@ -578,8 +582,13 @@ export interface EmitPluginAppEventArgs {
578
582
  eventData: Record<string, unknown>;
579
583
  }
580
584
  /**
581
- * Read one app's state folded to head, by nodeId — server truth for an op or
582
- * a task. Null when no such app exists. HOST-ONLY.
585
+ * Read one app's state folded to head, by nodeId — server truth for an op, route,
586
+ * task or webhook. It reads for the app instance of the call the platform is
587
+ * running: your own app and other instances of its type in your workspace; another
588
+ * app type only when plugin.json `workspaceTools` grants a tool of that type (the
589
+ * refusal names the line to add). Null when no such app exists — an app in another
590
+ * workspace is null too. A webhook (it runs for no instance) finds only instances
591
+ * of its own type. Outside a running call it throws. HOST-ONLY.
583
592
  */
584
593
  export declare function readAppState(_nodeId: string): Promise<{
585
594
  nodeId: string;
@@ -658,6 +667,67 @@ export declare function mintRouteToken(_ctx: {
658
667
  ttlSeconds?: number;
659
668
  label?: string;
660
669
  }): Promise<RouteTokenGrant>;
670
+ /** A question for the person (questions(ctx).ask). */
671
+ export interface AppQuestion {
672
+ /** Stable per question within this app instance: asking twice with one key is one question. */
673
+ key: string;
674
+ question: string;
675
+ /** 1–4 answers the bell offers as buttons; without them the person types. */
676
+ options?: string[];
677
+ /** Context the bell shows under the question (≤1500 characters as JSON). */
678
+ details?: Record<string, unknown>;
679
+ /** Where the bell's Open button takes the person: the app hears `{ [kind]: key }` through useAppNav. */
680
+ open?: {
681
+ kind: string;
682
+ key: string;
683
+ label?: string;
684
+ };
685
+ /** The app's own op that takes the answer, called as the person answering with `{ ...args, answer }`. */
686
+ answer: {
687
+ op: string;
688
+ args?: Record<string, unknown>;
689
+ };
690
+ expiresInSeconds?: number;
691
+ }
692
+ export interface Questions {
693
+ ask(q: AppQuestion): Promise<{
694
+ questionId: string;
695
+ created: boolean;
696
+ }>;
697
+ /** The app settled it itself (answered on its own screen, the asker ended): the bell's row closes. */
698
+ settle(key: string, outcome: {
699
+ answer: string;
700
+ } | {
701
+ cancelled: true;
702
+ }): Promise<void>;
703
+ }
704
+ /**
705
+ * THE PLATFORM'S QUESTIONS BELL. A question an app needs the person to answer
706
+ * (an agent waiting on a decision) reaches them wherever they are — the bell,
707
+ * on every workspace — not only when they open the app:
708
+ *
709
+ * await questions(ctx).ask({
710
+ * key: `thread:${threadId}:${questionId}`,
711
+ * question: "Accept 12 % off if they order 500?",
712
+ * options: ["Yes", "No"],
713
+ * open: { kind: "thread", key: threadId, label: "Open email thread" },
714
+ * answer: { op: "answer_question", args: { threadId, questionId } },
715
+ * });
716
+ *
717
+ * Answering in the bell calls `answer.op` with `{ ...args, answer }` as the
718
+ * person who answered (their access, the op's own rules); an `{ ok: false }`
719
+ * reply closes the row and shows the app's reason. When the app takes the
720
+ * answer on its own screen, `settle(key, { answer })` closes the row without a
721
+ * call back. HOST-ONLY; the Forge box keeps no bell (ask answers `created: false`).
722
+ */
723
+ export declare function questions(_ctx: {
724
+ pluginId: string;
725
+ workspaceId: string;
726
+ nodeId: string;
727
+ viewer?: {
728
+ userId: string | null;
729
+ };
730
+ }): Questions;
661
731
  /**
662
732
  * PARTS OF THE APP THAT RUN ON THE PERSON'S COMPUTER (plugin-device-arm.md).
663
733
  * The app declares its commands in plugin.json `device.commands`; the owner
package/dist/server.js CHANGED
@@ -202,8 +202,13 @@ export function removeAppRole(_ctx, _name) {
202
202
  return hostOnly("removeAppRole");
203
203
  }
204
204
  /**
205
- * Read one app's state folded to head, by nodeId — server truth for an op or
206
- * a task. Null when no such app exists. HOST-ONLY.
205
+ * Read one app's state folded to head, by nodeId — server truth for an op, route,
206
+ * task or webhook. It reads for the app instance of the call the platform is
207
+ * running: your own app and other instances of its type in your workspace; another
208
+ * app type only when plugin.json `workspaceTools` grants a tool of that type (the
209
+ * refusal names the line to add). Null when no such app exists — an app in another
210
+ * workspace is null too. A webhook (it runs for no instance) finds only instances
211
+ * of its own type. Outside a running call it throws. HOST-ONLY.
207
212
  */
208
213
  export function readAppState(_nodeId) {
209
214
  return hostOnly("readAppState");
@@ -256,6 +261,28 @@ export function renderChartImage(_ctx, _args) {
256
261
  export function mintRouteToken(_ctx, _args) {
257
262
  return hostOnly("mintRouteToken");
258
263
  }
264
+ /**
265
+ * THE PLATFORM'S QUESTIONS BELL. A question an app needs the person to answer
266
+ * (an agent waiting on a decision) reaches them wherever they are — the bell,
267
+ * on every workspace — not only when they open the app:
268
+ *
269
+ * await questions(ctx).ask({
270
+ * key: `thread:${threadId}:${questionId}`,
271
+ * question: "Accept 12 % off if they order 500?",
272
+ * options: ["Yes", "No"],
273
+ * open: { kind: "thread", key: threadId, label: "Open email thread" },
274
+ * answer: { op: "answer_question", args: { threadId, questionId } },
275
+ * });
276
+ *
277
+ * Answering in the bell calls `answer.op` with `{ ...args, answer }` as the
278
+ * person who answered (their access, the op's own rules); an `{ ok: false }`
279
+ * reply closes the row and shows the app's reason. When the app takes the
280
+ * answer on its own screen, `settle(key, { answer })` closes the row without a
281
+ * call back. HOST-ONLY; the Forge box keeps no bell (ask answers `created: false`).
282
+ */
283
+ export function questions(_ctx) {
284
+ return hostOnly("questions");
285
+ }
259
286
  /**
260
287
  * PARTS OF THE APP THAT RUN ON THE PERSON'S COMPUTER (plugin-device-arm.md).
261
288
  * The app declares its commands in plugin.json `device.commands`; the owner
package/dist/types.d.ts CHANGED
@@ -106,6 +106,15 @@ export interface EventDefinition<StateType extends ApplicationIdentifier> {
106
106
  triggerMeta?: TriggerMeta;
107
107
  sideEffect?: EventSideEffect;
108
108
  permission?: "user_exclusive";
109
+ /**
110
+ * WHO MAY APPEND IT. `"server"`: this event records something only the app's own server code
111
+ * knows — a message sent, a sync's result, a model's output, a payment. It is appended by the
112
+ * app's ops, routes, tasks and webhooks (and the platform), never by a client: the browser's
113
+ * append and WAL doors and the PAT/SDK dispatch routes refuse it, so nobody can plant a "sent"
114
+ * that was never sent or a sync that never ran (the platform's mail requirements, KD-120). Absent: any writer
115
+ * with access to the app may append it, as before.
116
+ */
117
+ origin?: "server";
109
118
  conflictPolicy?: "mark" | "silent";
110
119
  /** The sub-items (e.g. block ids) a partial write touches — two devices
111
120
  * writing different sub-items of one item merge without a conflict. */
@@ -88,13 +88,20 @@ snapshot baselines and the durability layers. Every shipped plugin and the scaff
88
88
 
89
89
  ## Events only the server emits
90
90
 
91
- There is no "server-only" event type: `EventTypes` has `Client` (what the UI and tools dispatch,
92
- and what a task's `dispatchEvent` or an op's `ctx.emit` writes through the same processors),
93
- `Workflow` and `Workspace` (the platform's own). An event that only a task or a webhook should
94
- produce is declared like any other; the guard is where its `dataCreator` is called — keep it out
95
- of the UI's reach and say so in a comment. (An earlier version of this page named
96
- `EventTypes.Server`, which does not exist: the Pantry's first type check in a box found it,
97
- 2026-09-28.) The processor still obeys the three rules.
91
+ An event that records something only your server code knows — a letter sent, a sync's result, a
92
+ model's answer, a payment — says so with `origin: "server"`:
93
+
94
+ ```ts
95
+ { eventName: "mail_sent", type: EventTypes.Client, origin: "server", dataCreator, processor }
96
+ ```
97
+
98
+ Your ops, routes, tasks and webhooks append it as usual (`ctx.emit`, a task's `dispatchEvent`).
99
+ Every door a CLIENT's event enters by refuses it — the browser's appends and offline queue, and the
100
+ PAT / SDK / MCP dispatch routes — so nobody with write access can plant a "sent" that was never
101
+ sent or a sync that never ran. Your UI can still predict it for an optimistic click (run the
102
+ processor locally); it just never dispatches it. `EventTypes` has no server type for this
103
+ (`Client`, `Workflow`, `Workspace` only — an earlier version of this page named `EventTypes.Server`,
104
+ which does not exist); `origin` is the switch. The processor still obeys the three rules.
98
105
 
99
106
  ## Durability you get for free
100
107
 
package/docs/04-tools.md CHANGED
@@ -100,6 +100,33 @@ a `PluginCallError` carrying the platform's `code` (`forbidden`, `login-required
100
100
  pass it to `useSignInWall().raise(err)` in the UI and a `login-required` becomes the sign-in wall
101
101
  instead of an error. Relay any other error's message; never swallow it.
102
102
 
103
+ ## Waiting inside a network run — `mailWaitDirective`
104
+
105
+ Inside an agent network run (`runCtx` is the fifth argument of `toolkitCreator`; absent in chat and
106
+ voice) a tool can PAUSE the run until mail arrives — minutes or days, nothing running — with the
107
+ platform's own wait kernel:
108
+
109
+ ```ts
110
+ import { mailWaitDirective } from "esoul-sdk";
111
+
112
+ execute: async (args) => {
113
+ if (!runCtx) return "Only inside an agent network run.";
114
+ if (runCtx.isWaitDirectiveAlreadyCreatedThisStep()) return "One wait per turn.";
115
+ eventCallback(mailWaitDirective(runCtx, {
116
+ workspaceId, inboxNodeId: identifier.nodeId, inboxName: identifier.instanceName,
117
+ toolName: `wait_for_email_${base}`, threadId: args.threadId,
118
+ afterTimestamp, // the newest message the agent has already seen
119
+ directiveId, // stable for a retried call
120
+ }));
121
+ runCtx.setWaitDirectiveCreatedThisStep();
122
+ return "Waiting…";
123
+ }
124
+ ```
125
+
126
+ The run wakes on `workspace/email.message_arrived` for this inbox (the platform sends it for the
127
+ Gmail app's live mail) and first checks the inbox's folded `messages` for an arrival newer than
128
+ `afterTimestamp`, so a reply that landed before the wait is not missed.
129
+
103
130
  ## Calling other apps
104
131
 
105
132
  From the UI: `useWorkspaceTools()` (docs/05). From server code: `callWorkspaceTool` (docs/06).
package/docs/05-ui.md CHANGED
@@ -165,6 +165,26 @@ Reload in place, without a skeleton: a screen that flashes every time the assist
165
165
  reads as broken. Check it in the workbench: open the preview, call one of your write tools from the
166
166
  board (or `esoul-forge tool …`), and watch the view change within a few seconds, without a reload.
167
167
 
168
+ ## Opened from outside: a place inside your app
169
+
170
+ The platform's tasks pane (and, as they adopt it, link chips and memory hits) can open a place
171
+ INSIDE your app — a conversation, a campaign, a page — not just the app. It names the place with
172
+ keys you choose; you hear it with `useAppNav`:
173
+
174
+ ```tsx
175
+ import { useAppNav } from "esoul-sdk/react";
176
+
177
+ useAppNav(nodeId, (target) => {
178
+ if (target.thread) openThread(target.thread);
179
+ else if (target.campaign) openCampaign(target.campaign);
180
+ });
181
+ ```
182
+
183
+ `onTarget` runs with the place waiting when the app mounts (declare it after any effect that
184
+ restores a remembered view, so the request wins) and with each one asked for while the app is on
185
+ screen. An unknown key does nothing. Name the keys in your task descriptions so the platform can
186
+ point at them.
187
+
168
188
  ## Cross-app from the UI
169
189
 
170
190
  ```ts
package/docs/06-server.md CHANGED
@@ -61,6 +61,31 @@ One app, folded to head: `{ nodeId, workspaceId, applicationType, foldedSeq, sta
61
61
  when absent. Use it for your own instance and, with care, for other apps in the same workspace.
62
62
  Never a raw database read — the import wall refuses `prisma` and the state column lags the log.
63
63
 
64
+ ## Asking the person — `questions(ctx)`
65
+
66
+ A question your app needs answered (an agent waiting on a decision) goes to the platform's
67
+ questions bell, which the person sees on every workspace — not only when your app is open:
68
+
69
+ ```ts
70
+ import { questions } from "esoul-sdk/server";
71
+
72
+ await questions(ctx).ask({
73
+ key: `thread:${threadId}:${questionId}`, // one question per key: a replayed ask is the same one
74
+ question: "Accept 12 % off if they order 500?",
75
+ options: ["Yes", "No"], // 1–4 → buttons; without them the person types
76
+ open: { kind: "thread", key: threadId, label: "Open email thread" },
77
+ answer: { op: "answer_question", args: { threadId, questionId, via: "bell" } },
78
+ });
79
+ ```
80
+
81
+ - Answered in the bell, the platform calls **your own op** (`answer.op`, one of your declared ops)
82
+ with `{ ...args, answer }`, as the person who answered — their access, your op's rules. Reply
83
+ `{ ok: false, error }` when the question is gone: the bell closes it and shows your words.
84
+ - `open` is where the bell's Open button lands: your app hears `{ [kind]: key }` through `useAppNav`.
85
+ - Answered on your own screen instead, close the bell's copy: `questions(ctx).settle(key, { answer })`
86
+ (or `{ cancelled: true }`). The platform does not call you back for a settle.
87
+ - In a Forge box there is no bell: `ask` answers `{ created: false }` and your screen still shows it.
88
+
64
89
  ## `callWorkspaceTool` — orchestrate other apps
65
90
 
66
91
  ```ts
package/llms-full.txt CHANGED
@@ -880,13 +880,20 @@ snapshot baselines and the durability layers. Every shipped plugin and the scaff
880
880
 
881
881
  ## Events only the server emits
882
882
 
883
- There is no "server-only" event type: `EventTypes` has `Client` (what the UI and tools dispatch,
884
- and what a task's `dispatchEvent` or an op's `ctx.emit` writes through the same processors),
885
- `Workflow` and `Workspace` (the platform's own). An event that only a task or a webhook should
886
- produce is declared like any other; the guard is where its `dataCreator` is called — keep it out
887
- of the UI's reach and say so in a comment. (An earlier version of this page named
888
- `EventTypes.Server`, which does not exist: the Pantry's first type check in a box found it,
889
- 2026-09-28.) The processor still obeys the three rules.
883
+ An event that records something only your server code knows — a letter sent, a sync's result, a
884
+ model's answer, a payment — says so with `origin: "server"`:
885
+
886
+ ```ts
887
+ { eventName: "mail_sent", type: EventTypes.Client, origin: "server", dataCreator, processor }
888
+ ```
889
+
890
+ Your ops, routes, tasks and webhooks append it as usual (`ctx.emit`, a task's `dispatchEvent`).
891
+ Every door a CLIENT's event enters by refuses it — the browser's appends and offline queue, and the
892
+ PAT / SDK / MCP dispatch routes — so nobody with write access can plant a "sent" that was never
893
+ sent or a sync that never ran. Your UI can still predict it for an optimistic click (run the
894
+ processor locally); it just never dispatches it. `EventTypes` has no server type for this
895
+ (`Client`, `Workflow`, `Workspace` only — an earlier version of this page named `EventTypes.Server`,
896
+ which does not exist); `origin` is the switch. The processor still obeys the three rules.
890
897
 
891
898
  ## Durability you get for free
892
899
 
@@ -1001,6 +1008,33 @@ a `PluginCallError` carrying the platform's `code` (`forbidden`, `login-required
1001
1008
  pass it to `useSignInWall().raise(err)` in the UI and a `login-required` becomes the sign-in wall
1002
1009
  instead of an error. Relay any other error's message; never swallow it.
1003
1010
 
1011
+ ## Waiting inside a network run — `mailWaitDirective`
1012
+
1013
+ Inside an agent network run (`runCtx` is the fifth argument of `toolkitCreator`; absent in chat and
1014
+ voice) a tool can PAUSE the run until mail arrives — minutes or days, nothing running — with the
1015
+ platform's own wait kernel:
1016
+
1017
+ ```ts
1018
+ import { mailWaitDirective } from "esoul-sdk";
1019
+
1020
+ execute: async (args) => {
1021
+ if (!runCtx) return "Only inside an agent network run.";
1022
+ if (runCtx.isWaitDirectiveAlreadyCreatedThisStep()) return "One wait per turn.";
1023
+ eventCallback(mailWaitDirective(runCtx, {
1024
+ workspaceId, inboxNodeId: identifier.nodeId, inboxName: identifier.instanceName,
1025
+ toolName: `wait_for_email_${base}`, threadId: args.threadId,
1026
+ afterTimestamp, // the newest message the agent has already seen
1027
+ directiveId, // stable for a retried call
1028
+ }));
1029
+ runCtx.setWaitDirectiveCreatedThisStep();
1030
+ return "Waiting…";
1031
+ }
1032
+ ```
1033
+
1034
+ The run wakes on `workspace/email.message_arrived` for this inbox (the platform sends it for the
1035
+ Gmail app's live mail) and first checks the inbox's folded `messages` for an arrival newer than
1036
+ `afterTimestamp`, so a reply that landed before the wait is not missed.
1037
+
1004
1038
  ## Calling other apps
1005
1039
 
1006
1040
  From the UI: `useWorkspaceTools()` (docs/05). From server code: `callWorkspaceTool` (docs/06).
@@ -1190,6 +1224,26 @@ Reload in place, without a skeleton: a screen that flashes every time the assist
1190
1224
  reads as broken. Check it in the workbench: open the preview, call one of your write tools from the
1191
1225
  board (or `esoul-forge tool …`), and watch the view change within a few seconds, without a reload.
1192
1226
 
1227
+ ## Opened from outside: a place inside your app
1228
+
1229
+ The platform's tasks pane (and, as they adopt it, link chips and memory hits) can open a place
1230
+ INSIDE your app — a conversation, a campaign, a page — not just the app. It names the place with
1231
+ keys you choose; you hear it with `useAppNav`:
1232
+
1233
+ ```tsx
1234
+ import { useAppNav } from "esoul-sdk/react";
1235
+
1236
+ useAppNav(nodeId, (target) => {
1237
+ if (target.thread) openThread(target.thread);
1238
+ else if (target.campaign) openCampaign(target.campaign);
1239
+ });
1240
+ ```
1241
+
1242
+ `onTarget` runs with the place waiting when the app mounts (declare it after any effect that
1243
+ restores a remembered view, so the request wins) and with each one asked for while the app is on
1244
+ screen. An unknown key does nothing. Name the keys in your task descriptions so the platform can
1245
+ point at them.
1246
+
1193
1247
  ## Cross-app from the UI
1194
1248
 
1195
1249
  ```ts
@@ -1306,6 +1360,31 @@ One app, folded to head: `{ nodeId, workspaceId, applicationType, foldedSeq, sta
1306
1360
  when absent. Use it for your own instance and, with care, for other apps in the same workspace.
1307
1361
  Never a raw database read — the import wall refuses `prisma` and the state column lags the log.
1308
1362
 
1363
+ ## Asking the person — `questions(ctx)`
1364
+
1365
+ A question your app needs answered (an agent waiting on a decision) goes to the platform's
1366
+ questions bell, which the person sees on every workspace — not only when your app is open:
1367
+
1368
+ ```ts
1369
+ import { questions } from "esoul-sdk/server";
1370
+
1371
+ await questions(ctx).ask({
1372
+ key: `thread:${threadId}:${questionId}`, // one question per key: a replayed ask is the same one
1373
+ question: "Accept 12 % off if they order 500?",
1374
+ options: ["Yes", "No"], // 1–4 → buttons; without them the person types
1375
+ open: { kind: "thread", key: threadId, label: "Open email thread" },
1376
+ answer: { op: "answer_question", args: { threadId, questionId, via: "bell" } },
1377
+ });
1378
+ ```
1379
+
1380
+ - Answered in the bell, the platform calls **your own op** (`answer.op`, one of your declared ops)
1381
+ with `{ ...args, answer }`, as the person who answered — their access, your op's rules. Reply
1382
+ `{ ok: false, error }` when the question is gone: the bell closes it and shows your words.
1383
+ - `open` is where the bell's Open button lands: your app hears `{ [kind]: key }` through `useAppNav`.
1384
+ - Answered on your own screen instead, close the bell's copy: `questions(ctx).settle(key, { answer })`
1385
+ (or `{ cancelled: true }`). The platform does not call you back for a settle.
1386
+ - In a Forge box there is no bell: `ask` answers `{ created: false }` and your screen still shows it.
1387
+
1309
1388
  ## `callWorkspaceTool` — orchestrate other apps
1310
1389
 
1311
1390
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esoul-sdk",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "description": "Build a full product on ExternalSoul: your own tables with per-person rules, a viewer on every seam, app roles, access levels, realtime with audiences, durable tasks, and bindings to other apps.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",