@introspection-ai/recipes 0.21.0 → 0.22.1

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 (64) hide show
  1. package/README.md +5 -0
  2. package/dist/channels/index.d.ts +7 -0
  3. package/dist/channels/index.d.ts.map +1 -0
  4. package/dist/channels/index.js +4 -0
  5. package/dist/channels/index.js.map +1 -0
  6. package/dist/channels/module.d.ts +43 -0
  7. package/dist/channels/module.d.ts.map +1 -0
  8. package/dist/channels/module.js +61 -0
  9. package/dist/channels/module.js.map +1 -0
  10. package/dist/channels/refs.d.ts +28 -0
  11. package/dist/channels/refs.d.ts.map +1 -0
  12. package/dist/channels/refs.js +91 -0
  13. package/dist/channels/refs.js.map +1 -0
  14. package/dist/channels/tools.d.ts +56 -0
  15. package/dist/channels/tools.d.ts.map +1 -0
  16. package/dist/channels/tools.js +325 -0
  17. package/dist/channels/tools.js.map +1 -0
  18. package/dist/channels/types.d.ts +209 -0
  19. package/dist/channels/types.d.ts.map +1 -0
  20. package/dist/channels/types.js +23 -0
  21. package/dist/channels/types.js.map +1 -0
  22. package/dist/connector-tools.d.ts +37 -0
  23. package/dist/connector-tools.d.ts.map +1 -0
  24. package/dist/connector-tools.js +88 -0
  25. package/dist/connector-tools.js.map +1 -0
  26. package/dist/mcp-chunks/{chunk-XK3LRH3Q.js → chunk-JZDXKYQM.js} +1 -1
  27. package/dist/mcp-daemon.js +1 -1
  28. package/dist/mcp-run-worker.js +1 -1
  29. package/dist/mcp-tools.d.ts +1 -6
  30. package/dist/mcp-tools.d.ts.map +1 -1
  31. package/dist/mcp-tools.js +5 -73
  32. package/dist/mcp-tools.js.map +1 -1
  33. package/dist/pi-extension.d.ts.map +1 -1
  34. package/dist/pi-extension.js +63 -13
  35. package/dist/pi-extension.js.map +1 -1
  36. package/dist/recipe-agent.d.ts.map +1 -1
  37. package/dist/recipe-agent.js +3 -2
  38. package/dist/recipe-agent.js.map +1 -1
  39. package/dist/recipe-extensions.d.ts +1 -0
  40. package/dist/recipe-extensions.d.ts.map +1 -1
  41. package/dist/recipe-extensions.js +32 -11
  42. package/dist/recipe-extensions.js.map +1 -1
  43. package/dist/recipe-package.d.ts +5 -0
  44. package/dist/recipe-package.d.ts.map +1 -1
  45. package/dist/recipe-package.js +68 -3
  46. package/dist/recipe-package.js.map +1 -1
  47. package/dist/session.d.ts.map +1 -1
  48. package/dist/session.js +71 -16
  49. package/dist/session.js.map +1 -1
  50. package/dist/test-utils.d.ts +1 -0
  51. package/dist/test-utils.d.ts.map +1 -1
  52. package/dist/test-utils.js +1 -0
  53. package/dist/test-utils.js.map +1 -1
  54. package/dist/tool-search.d.ts +21 -0
  55. package/dist/tool-search.d.ts.map +1 -0
  56. package/dist/tool-search.js +176 -0
  57. package/dist/tool-search.js.map +1 -0
  58. package/docs/channels.md +167 -0
  59. package/docs/index.md +2 -0
  60. package/docs/mcp-configuration.md +6 -4
  61. package/docs/pi-extension.md +4 -3
  62. package/docs/recipe-format.md +37 -0
  63. package/docs/slack.md +135 -0
  64. package/package.json +17 -12
@@ -0,0 +1,176 @@
1
+ import { Type } from "typebox";
2
+ export const RECIPE_TOOL_SEARCH_NAME = "tool_search";
3
+ export const LEGACY_MCP_TOOL_SEARCH_NAME = "mcp_search";
4
+ function words(value) {
5
+ return value
6
+ .toLowerCase()
7
+ .split(/[^a-z0-9]+/)
8
+ .filter(Boolean);
9
+ }
10
+ function schemaSearchTerms(schema) {
11
+ const propertyNames = [];
12
+ const propertyDescriptions = [];
13
+ const visited = new Set();
14
+ const visit = (value) => {
15
+ if (!value || typeof value !== "object" || visited.has(value))
16
+ return;
17
+ visited.add(value);
18
+ const record = value;
19
+ if (record.properties && typeof record.properties === "object") {
20
+ for (const [name, property] of Object.entries(record.properties)) {
21
+ propertyNames.push(name);
22
+ if (property && typeof property === "object") {
23
+ const description = property.description;
24
+ if (typeof description === "string") {
25
+ propertyDescriptions.push(description);
26
+ }
27
+ }
28
+ visit(property);
29
+ }
30
+ }
31
+ for (const keyword of [
32
+ "allOf",
33
+ "anyOf",
34
+ "oneOf",
35
+ "items",
36
+ "additionalProperties",
37
+ ]) {
38
+ const nested = record[keyword];
39
+ if (Array.isArray(nested)) {
40
+ for (const entry of nested)
41
+ visit(entry);
42
+ }
43
+ else {
44
+ visit(nested);
45
+ }
46
+ }
47
+ };
48
+ visit(schema);
49
+ return {
50
+ propertyNames: propertyNames.join(" ").toLowerCase(),
51
+ propertyDescriptions: propertyDescriptions.join(" ").toLowerCase(),
52
+ };
53
+ }
54
+ function scoreTool(tool, query) {
55
+ const normalizedQuery = query.trim().toLowerCase();
56
+ if (!normalizedQuery)
57
+ return 0;
58
+ const name = tool.name.toLowerCase();
59
+ const label = (tool.label ?? tool.name).toLowerCase();
60
+ const description = tool.description.toLowerCase();
61
+ const { propertyNames, propertyDescriptions } = schemaSearchTerms(tool.parameters);
62
+ let score = 0;
63
+ if (name === normalizedQuery)
64
+ score += 500;
65
+ if (label === normalizedQuery)
66
+ score += 300;
67
+ if (name.includes(normalizedQuery))
68
+ score += 120;
69
+ if (label.includes(normalizedQuery))
70
+ score += 100;
71
+ if (description.includes(normalizedQuery))
72
+ score += 60;
73
+ const nameTerms = new Set(words(name));
74
+ const labelTerms = new Set(words(label));
75
+ const propertyNameTerms = new Set(words(propertyNames));
76
+ for (const term of words(normalizedQuery)) {
77
+ if (nameTerms.has(term))
78
+ score += 40;
79
+ else if (name.includes(term))
80
+ score += 20;
81
+ if (labelTerms.has(term))
82
+ score += 30;
83
+ else if (label.includes(term))
84
+ score += 15;
85
+ if (description.includes(term))
86
+ score += 16;
87
+ if (propertyNameTerms.has(term))
88
+ score += 12;
89
+ else if (propertyNames.includes(term))
90
+ score += 6;
91
+ if (propertyDescriptions.includes(term))
92
+ score += 6;
93
+ }
94
+ return score;
95
+ }
96
+ export function createRecipeToolSearch(options) {
97
+ const toolsByName = new Map(options.tools.map((tool) => [tool.name, tool]));
98
+ const deferred = [...new Set(options.deferredToolNames)].map((name) => {
99
+ const tool = toolsByName.get(name);
100
+ if (!tool) {
101
+ throw new Error(`Recipe deferred tool '${name}' was not registered before tool search`);
102
+ }
103
+ return tool;
104
+ });
105
+ if (deferred.length === 0)
106
+ return undefined;
107
+ if (toolsByName.has(RECIPE_TOOL_SEARCH_NAME)) {
108
+ throw new Error(`Recipe tool name '${RECIPE_TOOL_SEARCH_NAME}' is reserved by the session`);
109
+ }
110
+ return {
111
+ name: RECIPE_TOOL_SEARCH_NAME,
112
+ label: "Tool search",
113
+ description: "Search inactive tools allowed for this Recipe and enable the best matches for the next model request.",
114
+ parameters: Type.Object({
115
+ query: Type.String({
116
+ description: "Capability or task to find a tool for.",
117
+ }),
118
+ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 10, default: 3 })),
119
+ }),
120
+ executionMode: "sequential",
121
+ async execute(_toolCallId, params) {
122
+ const input = params;
123
+ const query = typeof input.query === "string" ? input.query : "";
124
+ const limit = typeof input.limit === "number" ? input.limit : 3;
125
+ const active = new Set(options.activation.getActiveTools());
126
+ const matches = deferred
127
+ .filter((tool) => !active.has(tool.name))
128
+ .map((tool) => ({ tool, score: scoreTool(tool, query) }))
129
+ .filter((match) => match.score > 0)
130
+ .sort((left, right) => right.score - left.score ||
131
+ left.tool.name.localeCompare(right.tool.name))
132
+ .slice(0, limit);
133
+ const added = matches.map((match) => match.tool.name);
134
+ if (added.length > 0) {
135
+ options.activation.setActiveTools([...active, ...added]);
136
+ }
137
+ const details = {
138
+ matches: matches.map(({ tool }) => ({
139
+ name: tool.name,
140
+ label: tool.label ?? tool.name,
141
+ description: tool.description,
142
+ })),
143
+ added,
144
+ };
145
+ const text = matches.length === 0
146
+ ? `No inactive Recipe tools matched "${query}".`
147
+ : [
148
+ `Enabled ${matches.length} Recipe tool(s) for the next model request:`,
149
+ ...matches.map(({ tool }) => `- ${tool.name}${tool.description ? `: ${tool.description}` : ""}`),
150
+ ].join("\n");
151
+ return {
152
+ content: [{ type: "text", text }],
153
+ details,
154
+ };
155
+ },
156
+ };
157
+ }
158
+ export function createRecipeToolSearchTools(options, includeLegacyMcpAlias) {
159
+ const search = createRecipeToolSearch(options);
160
+ if (!search)
161
+ return [];
162
+ if (!includeLegacyMcpAlias)
163
+ return [search];
164
+ if (options.tools.some((tool) => tool.name === LEGACY_MCP_TOOL_SEARCH_NAME)) {
165
+ throw new Error(`Recipe tool name '${LEGACY_MCP_TOOL_SEARCH_NAME}' is reserved by the session`);
166
+ }
167
+ return [
168
+ search,
169
+ {
170
+ ...search,
171
+ name: LEGACY_MCP_TOOL_SEARCH_NAME,
172
+ label: "MCP search",
173
+ },
174
+ ];
175
+ }
176
+ //# sourceMappingURL=tool-search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-search.js","sourceRoot":"","sources":["../src/tool-search.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,167 @@
1
+ # Channel tools
2
+
3
+ A Recipe that answers a chat message declares a **channel connector**. The host
4
+ then registers a fixed set of `channel_*` tools, named and shaped identically
5
+ for every provider, and bound to the one conversation the task came from.
6
+
7
+ Two properties follow from that, and both are structural rather than
8
+ conventional:
9
+
10
+ - **The agent cannot address anything else.** No `channel_*` tool takes a
11
+ channel, thread, workspace, or user argument. The conversation is closed over
12
+ by the host from the task origin, so a compromised prompt has no vocabulary
13
+ for "post this somewhere else". The invariant is asserted by a test that
14
+ walks every registered tool's input schema.
15
+ - **A tool that the provider cannot support is absent, not failing.** Each
16
+ adapter declares a capability descriptor, and registration filters on it.
17
+ For example, an adapter with no history API has no `channel_read` tool.
18
+
19
+ Before the first model call, the channel extension adds origin metadata to the
20
+ system prompt. The metadata contains the provider, the conversation name and
21
+ permalink when the adapter can resolve them, whether the origin is a thread,
22
+ and the available channel tools. It contains no provider conversation IDs and
23
+ no messages. The injected guidance tells the agent to deliver its user-facing
24
+ response with `channel_reply`, because a normal final assistant response is not
25
+ delivered to the originating channel. `channel_read` remains the only way to
26
+ fetch earlier messages when the provider supports it.
27
+
28
+ ## The tools
29
+
30
+ | Tool | Model arguments | Requires |
31
+ | --- | --- | --- |
32
+ | `channel_reply` | `text` (Markdown) | always |
33
+ | `channel_read` | `limit?`, `cursor?` | `read` |
34
+ | `channel_react` | `message`, `emoji`, `action?` (`add` or `remove`) | `react` |
35
+ | `channel_edit` | `message`, `text` | `edit` |
36
+ | `channel_retract` | `message` | `retract` |
37
+ | `channel_attach` | `path`, `title?`, `comment?` | `attach` |
38
+ | `channel_fetch_file` | `file` (a `file_…` handle), `variant?` | `fetch_file` |
39
+ | `channel_post_document` | `title`, `markdown` | `documents` |
40
+
41
+ `channel_reply`, `channel_read`, and `channel_react` are active by default when
42
+ the provider supports them. The other selected tools start inactive, and the
43
+ model can find them through `tool_search`.
44
+
45
+ Reply content is **Markdown**. Each adapter renders it in the provider's own
46
+ format. There is no raw provider payload because the same tool contract must
47
+ work with every adapter.
48
+
49
+ ### Message and file references
50
+
51
+ `channel_react`, `channel_edit`, and `channel_retract` take a `message`, which is
52
+ an opaque handle minted by the host for the current session. It is not a Slack
53
+ timestamp or another provider message ID. Handles come back from
54
+ `channel_reply` and `channel_read`, so the model can only act on a message that
55
+ a channel tool returned.
56
+
57
+ `channel_react` adds a reaction when `action` is omitted. Set `action` to
58
+ `remove` to remove the agent's reaction with the same emoji.
59
+
60
+ Edit and retract have an extra check. They accept only a handle for a message
61
+ that this agent posted. Reading the same message again keeps its original
62
+ handle and authorship record.
63
+
64
+ `channel_fetch_file` works the same way. Attachments returned by `channel_read`
65
+ carry a `file_…` handle, and that is the only value the tool accepts. A bot can
66
+ usually read files from every conversation it belongs to, so accepting a raw
67
+ provider file ID would let the model reach files outside the bound conversation.
68
+
69
+ ### Enrichment, not lookup tools
70
+
71
+ Author names and permalinks are resolved by the adapter in trusted code and
72
+ attached to what the agent is already reading: `channel_read` rows carry
73
+ `author.display_name`, and `channel_reply` returns a `permalink` where the
74
+ provider has one. There is no `resolve_user` or `get_permalink` tool, because
75
+ each would require another model turn. Each lookup would also take an
76
+ addressing argument.
77
+
78
+ ### Unsupported operations
79
+
80
+ Workspace search, channel listing and joining, directory lookup, and posting to
81
+ another conversation are unsupported. The proposal does not choose an API or
82
+ access model for those operations. A separate proposal can define them when a
83
+ concrete use case requires them.
84
+
85
+ Typing indicators and presence are runtime effects rather than model decisions.
86
+ Streaming controls how a reply is delivered. The runtime derives idempotency
87
+ keys, and the webhook already removes duplicate inbound events.
88
+
89
+ ## Declare a channel connector
90
+
91
+ ```json
92
+ {
93
+ "dependencies": {
94
+ "@introspection-ai/recipe-channel-slack": "^0.1.0"
95
+ },
96
+ "pi": {
97
+ "connectors": [
98
+ {
99
+ "provider": "slack"
100
+ }
101
+ ]
102
+ }
103
+ }
104
+ ```
105
+
106
+ The connector declaration enables the provider package and its supported tool
107
+ catalog. The agent YAML file is the only place that narrows the catalog:
108
+
109
+ ```yaml
110
+ tools: [channel_reply, channel_read, channel_react, channel_edit, channel_retract, channel_fetch_file]
111
+ ```
112
+
113
+ The host fails when an agent selects a tool that the provider does not support.
114
+
115
+ Because the vocabulary is neutral, the same declaration and the same agent
116
+ prompt work against another provider by changing the dependency and `provider`.
117
+ Each provider package declares the capabilities that it can support.
118
+
119
+ ## Write an adapter
120
+
121
+ An adapter supplies transport and a capability descriptor. It writes no tool
122
+ schemas. The shared schema keeps providers from defining different forms of
123
+ the same operation, and it prevents a provider from adding an addressing
124
+ argument.
125
+
126
+ ```ts
127
+ import {
128
+ createChannelConnectorModule,
129
+ type ChannelAdapter,
130
+ } from "@introspection-ai/recipes/channels";
131
+
132
+ const capabilities = {
133
+ react: false, edit: true, retract: true, read: false,
134
+ attach: false, fetchFile: false,
135
+ documents: false, resolveAuthors: true, permalinks: false,
136
+ };
137
+
138
+ class MyAdapter implements ChannelAdapter {
139
+ readonly provider = "my-channel";
140
+ readonly capabilities = capabilities;
141
+ async reply(ctx, { text }) { /* post into ctx.target */ }
142
+ async edit(ctx, { ref, text }) { /* edit an agent-authored message */ }
143
+ async retract(ctx, { ref }) { /* retract an agent-authored message */ }
144
+ }
145
+
146
+ export default createChannelConnectorModule({
147
+ provider: "my-channel",
148
+ capabilities,
149
+ createSession: ({ env }) => ({
150
+ adapter: new MyAdapter(/* client from env */),
151
+ // A function, so a task with no channel origin still starts: the tools
152
+ // fail when called, the session does not fail to open.
153
+ target: () => resolveTargetFrom(env),
154
+ }),
155
+ });
156
+ ```
157
+
158
+ Registration checks that the adapter implements every method its capabilities
159
+ claim, so a descriptor cannot promise a tool the adapter does not have.
160
+
161
+ The result is an ordinary `RecipeConnectorModule`. Manifest validation, agent
162
+ tool selection, and `tool_search` work without provider-specific code in the
163
+ Recipe.
164
+
165
+ ## Provider pages
166
+
167
+ - [Slack](slack.md)
package/docs/index.md CHANGED
@@ -17,6 +17,8 @@ task lifecycle, protocols, and deployment.
17
17
  | Compose agents and subagents | [Agent composition](agent-composition.md) |
18
18
  | Ask for user input across hosts | [Interactions](interactions.md) |
19
19
  | Declare capability policy and bindings | [MCP configuration](mcp-configuration.md) |
20
+ | Answer a chat message from a Recipe | [Channel tools](channels.md) |
21
+ | Add Slack tools to a Recipe | [Slack channel connector](slack.md) |
20
22
  | Define portable evaluation judges | [Recipe judges](recipe-judges.md) |
21
23
 
22
24
  ## Boundary
@@ -91,10 +91,12 @@ to hide all authorized tools for a server, then optionally list exact tools in
91
91
  or a sole `"*"` selector. `eager` wins when a tool matches both fields, but
92
92
  neither field can authorize a tool excluded by `include`/`exclude`.
93
93
 
94
- Deferred tools remain authorized and discoverable. When at least one exists,
95
- Recipes registers `mcp_search`; calling it searches only the authorized
96
- deferred catalog and adds matches to Pi's current active tool set for the next
97
- model request. It never grants access beyond `servers`.
94
+ Deferred tools remain authorized and discoverable. When at least one connector
95
+ or MCP tool is deferred, Recipes registers `tool_search`. Calling it searches
96
+ the inactive tools already allowed for the agent and adds the best matches to
97
+ Pi's active tool set for the next model request. It never grants access beyond
98
+ the Recipe manifest and agent policy. Recipes also registers `mcp_search` as a
99
+ compatibility alias when MCP tools are deferred.
98
100
 
99
101
  `defer` and `eager` are invalid in CLI mode. An omitted agent `mcp` block
100
102
  inherits its base policy. Once a child declares `mcp`, the complete block
@@ -53,9 +53,10 @@ For the selected agent, the extension:
53
53
  1. reads the root `package.json#pi` resource declarations;
54
54
  2. resolves the agent YAML, including `from:` inheritance;
55
55
  3. selects the model, thinking level, and tool allowlist;
56
- 4. loads selected skills, package prompts, and the complete Recipe extension closure;
57
- 5. materializes declared MCP bindings from host or local configuration;
58
- 6. exposes only the declared subagents through the shared `agent` tool.
56
+ 4. loads providers from `pi.connectors` and registers the tools selected by the agent;
57
+ 5. loads selected skills, package prompts, and the complete Recipe extension closure;
58
+ 6. materializes declared MCP bindings from host or local configuration;
59
+ 7. exposes only the declared subagents through the shared `agent` tool.
59
60
 
60
61
  See [Recipe Format](recipe-format.md) for the authored contract and
61
62
  [Agent composition](agent-composition.md) for inheritance and selection.
@@ -81,6 +81,43 @@ MUST commit one supported dependency lockfile: `package-lock.json`,
81
81
  `npm-shrinkwrap.json`, `pnpm-lock.yaml`, or `yarn.lock`. npm lockfiles MUST
82
82
  carry the same package name and version as `package.json`.
83
83
 
84
+ ## Connector tools
85
+
86
+ `pi.connectors` declares official provider tools that the host may register for
87
+ the Recipe. The declaration does not contain a connector ID, workspace ID, or
88
+ credential. A host binds those values when it starts a task.
89
+
90
+ ```json
91
+ {
92
+ "dependencies": {
93
+ "@introspection-ai/recipe-channel-slack": "^0.1.0"
94
+ },
95
+ "pi": {
96
+ "connectors": [
97
+ {
98
+ "provider": "slack"
99
+ }
100
+ ]
101
+ }
102
+ }
103
+ ```
104
+
105
+ Each provider may appear once. The host derives the package name from the
106
+ provider. For example, `slack` resolves to
107
+ `@introspection-ai/recipe-channel-slack`. The package owns its tool catalog
108
+ and marks which tools are active by default.
109
+
110
+ The provider package must be in the Recipe's production dependencies and
111
+ lockfile. The host imports the package only when the Recipe declares the
112
+ provider. The agent YAML file is the only place that narrows the package tool
113
+ catalog. An agent lists each allowed tool by its full name, such as
114
+ `channel_reply`, in its `tools` list. The host fails when the agent names a
115
+ tool that the connector does not register.
116
+
117
+ Chat providers share one connector shape. A channel connector registers the
118
+ provider-neutral `channel_*` tools that its capabilities support. See
119
+ [Channel tools](channels.md).
120
+
84
121
  ## Agents
85
122
 
86
123
  `pi.agents` may declare YAML agent definitions explicitly. When omitted,
package/docs/slack.md ADDED
@@ -0,0 +1,135 @@
1
+ # Slack channel connector
2
+
3
+ `@introspection-ai/recipe-channel-slack` is the Slack adapter for the
4
+ [channel tools](channels.md). It supplies Slack Web API transport and a
5
+ capability descriptor; the tool names and schemas are the neutral `channel_*`
6
+ set, so a Recipe written against it is not written against Slack.
7
+
8
+ Slack sends inbound events to the existing Events API webhook. The tools make
9
+ ordinary HTTP requests to the Slack Web API with the bot that received the
10
+ task. The package does not use Socket Mode, WebSockets, or a streamed tool
11
+ protocol.
12
+
13
+ ## Declare it
14
+
15
+ ```json
16
+ {
17
+ "dependencies": {
18
+ "@introspection-ai/recipe-channel-slack": "^0.1.0"
19
+ },
20
+ "pi": {
21
+ "connectors": [
22
+ {
23
+ "provider": "slack"
24
+ }
25
+ ]
26
+ }
27
+ }
28
+ ```
29
+
30
+ Commit the package manager lockfile. The host loads the package only for a
31
+ Recipe that declares the connector.
32
+
33
+ The connector package provides the complete Slack tool catalog. Each agent
34
+ lists the exact `channel_*` tools it may call in its YAML file. `channel_reply`,
35
+ `channel_read`, and `channel_react` are active from the start. Other selected
36
+ tools are available through `tool_search`.
37
+
38
+ ## What Slack registers
39
+
40
+ | Tool | Slack operation |
41
+ | --- | --- |
42
+ | `channel_reply` | `chat.postMessage` into the origin channel and thread |
43
+ | `channel_read` | `conversations.replies` in a thread, else `conversations.history` |
44
+ | `channel_react` | `reactions.add` or `reactions.remove` |
45
+ | `channel_edit` | `chat.update` for a message the agent posted |
46
+ | `channel_retract` | `chat.delete` for a message the agent posted |
47
+ | `channel_fetch_file` | `files.info` plus a private file download |
48
+
49
+ Slack history returns at most 15 messages to the agent per call. For a thread,
50
+ the first call reads the thread and returns the newest messages. The adapter
51
+ keeps older messages in the current session, and the returned opaque cursor
52
+ pages backward through that cache without another `conversations.replies`
53
+ request.
54
+
55
+ The connector uses a customer owned internal Slack app. Slack gives internal
56
+ apps the larger `conversations.replies` page and rate limits needed to read a
57
+ thread before selecting its newest messages. The connector does not support a
58
+ commercially distributed Slack app outside the Slack Marketplace, because
59
+ Slack restricts those installations to 15 replies and one request per minute.
60
+
61
+ `channel_attach` and `channel_post_document` are not registered: `files.uploadV2`
62
+ and canvases are not implemented in this package yet, and the capability
63
+ descriptor says so rather than registering tools that fail.
64
+
65
+ None of these take a channel or thread argument. Every tool acts on the
66
+ conversation the task came from. Author display names (`users.info`) and
67
+ permalinks (`chat.getPermalink`) are resolved inside the adapter and attached to
68
+ message rows and reply results, so there is no user lookup or permalink tool.
69
+ Edit and retract also require an opaque reference for a message posted by this
70
+ agent. They cannot act on another author's message.
71
+
72
+ Workspace search, channel listing and joining, directory lookup, and
73
+ cross-channel posting are unsupported. Their contract and access model are
74
+ deferred to a separate proposal.
75
+
76
+ ## Cloud access
77
+
78
+ The Recipe never receives the Slack bot token. The adapter sends the task
79
+ locator to `INTROSPECTION_EGRESS_URL`, the provider proxy inside the
80
+ Introspection environment, with the Slack host as the proxy route. The proxy
81
+ verifies and removes the locator, checks the connector's granted scope and
82
+ allowed path, and adds the bot token before the request leaves for Slack.
83
+
84
+ The adapter refuses to send a task locator when the provider proxy URL is
85
+ missing. It never falls back to sending the locator to Slack.
86
+
87
+ After `channel_reply` succeeds in cloud, the adapter posts the `connector_posted`
88
+ task event to the Data Plane, which checks the agent session, current run,
89
+ provider, and origin channel before recording the new thread root. A later Slack
90
+ reply then resumes the same task.
91
+
92
+ Slack writes are attempted once. The adapter does not retry `chat.postMessage`,
93
+ because Slack accepts no idempotency key for it. If Slack accepts the post but
94
+ event recording fails, the tool returns the message reference and a
95
+ `bridge_error`. It does not post again.
96
+
97
+ ## Test with introspection dev
98
+
99
+ Run `introspection dev` from the Recipe repository. A Slack event sent to the
100
+ development runtime starts a cloud sandbox with the local Recipe overlay, so the
101
+ adapter uses the cloud task origin and provider proxy and needs no local Slack
102
+ credential. Use `introspection dev --logs` for sandbox logs.
103
+
104
+ ## Test with introspection local
105
+
106
+ An `introspection local` run has no inbound Slack event, cloud task origin, or
107
+ credential proxy. Install dependencies, then set a bot token and a conversation:
108
+
109
+ ```bash
110
+ pnpm install --frozen-lockfile
111
+ export SLACK_BOT_TOKEN='xoxb-...'
112
+ export SLACK_CHANNEL_ID='C0123456789'
113
+ export SLACK_THREAD_TS='1234567890.123456' # optional
114
+ introspection local -p 'Summarise this thread and reply.'
115
+ ```
116
+
117
+ Local tools call Slack directly with `SLACK_BOT_TOKEN`. Local posts create no
118
+ inbound task or reply bridge, because no Data Plane task exists.
119
+
120
+ ## File downloads
121
+
122
+ `channel_fetch_file` writes a file under the task files directory and returns its
123
+ path, media type, size, and SHA-256 digest. The bytes land in the workspace and
124
+ not in model context. It accepts only a `file_…` handle from a `channel_read`
125
+ attachment, so the bot's cross-channel file read is not reachable from model
126
+ input. On the wire it accepts only `files.slack.com` download URLs, rejects
127
+ redirects, caps the body at 100 MiB, checks the declared size, and removes
128
+ partial files after a failure. The `video_low` variant uses Slack's smaller MP4
129
+ rendition when one exists.
130
+
131
+ ## Direct host use
132
+
133
+ The package exports `SlackChannelAdapter`, `createSlackChannelSession` and
134
+ `slackChannelTarget` for custom hosts and tests, alongside the default
135
+ `slackRecipeConnectorModule`. A normal Recipe uses `pi.connectors` instead.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@introspection-ai/recipes",
3
- "version": "0.21.0",
3
+ "version": "0.22.1",
4
4
  "description": "The open format for vertical agents, built on Pi.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -58,6 +58,10 @@
58
58
  "types": "./dist/api/session.d.ts",
59
59
  "import": "./dist/api/session.js"
60
60
  },
61
+ "./channels": {
62
+ "types": "./dist/channels/index.d.ts",
63
+ "import": "./dist/channels/index.js"
64
+ },
61
65
  "./test-utils": {
62
66
  "types": "./dist/test-utils.d.ts",
63
67
  "import": "./dist/test-utils.js"
@@ -87,10 +91,10 @@
87
91
  "typebox": "*"
88
92
  },
89
93
  "devDependencies": {
90
- "@earendil-works/pi-agent-core": "0.84.2",
91
- "@earendil-works/pi-ai": "0.84.2",
92
- "@earendil-works/pi-coding-agent": "0.84.2",
93
- "@earendil-works/pi-tui": "0.84.2",
94
+ "@earendil-works/pi-agent-core": "0.84.3",
95
+ "@earendil-works/pi-ai": "0.84.3",
96
+ "@earendil-works/pi-coding-agent": "0.84.3",
97
+ "@earendil-works/pi-tui": "0.84.3",
94
98
  "@types/node": "^26.1.2",
95
99
  "esbuild": "^0.28.1",
96
100
  "typebox": "^1.0.56",
@@ -98,19 +102,20 @@
98
102
  "vitest": "^4.0.18"
99
103
  },
100
104
  "optionalDependencies": {
101
- "@introspection-ai/mcp-client-linux-x64": "0.21.0",
102
- "@introspection-ai/mcp-client-linux-arm64": "0.21.0",
103
- "@introspection-ai/mcp-client-darwin-arm64": "0.21.0",
104
- "@introspection-ai/mcp-client-darwin-x64": "0.21.0"
105
+ "@introspection-ai/mcp-client-linux-x64": "0.22.1",
106
+ "@introspection-ai/mcp-client-linux-arm64": "0.22.1",
107
+ "@introspection-ai/mcp-client-darwin-arm64": "0.22.1",
108
+ "@introspection-ai/mcp-client-darwin-x64": "0.22.1"
105
109
  },
106
110
  "scripts": {
107
111
  "build": "pnpm build:ts && pnpm build:native",
108
- "build:ts": "rm -rf dist && tsc && node scripts/build-mcp-daemon.mjs",
112
+ "build:workspace-packages": "pnpm --filter './packages/**' build",
113
+ "build:ts": "rm -rf dist && tsc && node scripts/build-mcp-daemon.mjs && pnpm build:workspace-packages",
109
114
  "build:native": "cargo build --release -p pi-mcp-client && node scripts/package-mcp-client.mjs",
110
115
  "bench:mcp-client": "pnpm build:ts && cargo build --release -p pi-mcp-client && node scripts/benchmark-mcp-client.mjs",
111
- "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
116
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json && pnpm --filter './packages/**' typecheck",
112
117
  "test": "pnpm build:ts && cargo test -p pi-mcp-client -p introspection-recipe-check && cargo build -p pi-mcp-client && MCP_CLIENT_BIN=target/debug/mcp-client node scripts/package-mcp-client.mjs && vitest run",
113
- "pack:check": "pnpm pack --dry-run --json | node scripts/check-npm-pack.mjs",
118
+ "pack:check": "pnpm pack --dry-run --json | node scripts/check-npm-pack.mjs && pnpm --filter './packages/**' pack:check",
114
119
  "clean": "rm -rf dist .turbo node_modules target vendor/mcp-client"
115
120
  }
116
121
  }