pi-roundtable 0.9.9 → 0.9.10
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 +6 -0
- package/docs/plugins.md +8 -3
- package/package.json +1 -1
- package/src/core/define.ts +8 -0
- package/src/core/domain/ports.ts +7 -0
- package/src/core/routing/conversation-turns.ts +7 -0
- package/src/core/runtime/pi-agent-runtime.ts +9 -3
- package/src/core/runtime/session-factory.ts +3 -0
- package/src/core/sessions.ts +6 -0
- package/src/core/tool-turn.ts +2 -0
- package/src/testing.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.9.10] - 2026-10-10
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `ToolTurn.origin`, `TurnRequest.origin`, `ConversationTurnInput.origin` and `SessionContext.origin()`: where a turn's request came from, as the transport that carried it reported it, such as the browser origin of a web chat connection (`https://chat.example.com`). A tool's `run` and its `hold` read it from the `ToolTurn` they receive, so a tool can label what it creates (an issue's source line) and fail closed when it is undefined. A claim passes it to `context.turns.run({ ..., origin })`; the runtime keeps it with the turn's speaker and clears it when the turn ends. Only the transport sets it, never the message text or the model. A turn whose channel gives none (Discord, a schedule, an agent's turn, the CLI) leaves it undefined, and `testPlugin().runTool` takes `origin` in its options. Additive: nothing changes for a claim or a tool that does not use it, and `SessionContext.origin` is optional on the type so a context built before this still type-checks.
|
|
13
|
+
|
|
8
14
|
## [0.9.9] - 2026-10-10
|
|
9
15
|
|
|
10
16
|
### Added
|
package/docs/plugins.md
CHANGED
|
@@ -589,6 +589,10 @@ The host logs counts only (`files`, `bytes`, `unattributedBytes`), never a file
|
|
|
589
589
|
`run` receives the arguments, already typed, and the turn: the speaker, the channel, the agent, an abort signal, `workspace`, `attachFile`, and `attachment`.
|
|
590
590
|
`turn.workspace` is `{ workspace, scratchDir? }` in a session with a shared workspace (an agent's), the same roots as `SessionContext.workspace`, and undefined elsewhere: a tool that makes a file can save it there, under the scratch dir when there is one, and name its path, so the model sends it on with `attach_file` or a `discord_send_message` file `path` instead of carrying its bytes.
|
|
591
591
|
It returns the text the model reads.
|
|
592
|
+
`turn.origin` is where the turn's request came from, as the transport that carried it reported it, and undefined when the channel gives none: the web chat sets it to the browser origin its connection opened with (`https://chat.example.com`, `chrome-extension://<id>`), a Discord message, a schedule or an agent's turn leave it undefined.
|
|
593
|
+
Only a channel's transport sets it, never the message text or the model, so a tool may write it on a record it creates, such as an issue's source line; a tool that needs it fails closed when it is undefined.
|
|
594
|
+
A claim passes it to `context.turns.run` as `origin`, and the hold of the same call sees it too, since `hold` receives the same `ToolTurn`.
|
|
595
|
+
|
|
592
596
|
`turn.attachment(file)` opens a file someone attached to the conversation, by the name the turn's `## Attachments` block lists, and returns `{ file, name, contentType, size, bytes() }`: a tool passes a user's file on without the model carrying its bytes.
|
|
593
597
|
Only the conversation's own files open; a path, a hidden name, or a missing file throws a `ToolRefusal` the model reads, and so does a file the host's [retention period](#retention-removing-used-files-after-a-period) removed, which says so.
|
|
594
598
|
Throw `ToolRefusal` for a call the model should correct; any other error fails the call.
|
|
@@ -746,7 +750,7 @@ export const cleanup = definePlugin({
|
|
|
746
750
|
#### A hold that looks things up
|
|
747
751
|
|
|
748
752
|
A hold may need something it must fetch as the speaker before it can describe the call, such as the display name of the entity an id stands for, read with the speaker's own credentials.
|
|
749
|
-
A tool's `hold` may then return a promise and takes the turn as a second argument: the `ToolTurn` its `run` receives, with the `speaker`, the `channel` and the `
|
|
753
|
+
A tool's `hold` may then return a promise and takes the turn as a second argument: the `ToolTurn` its `run` receives, with the `speaker`, the `channel`, the `agent` and the `origin` of the turn that makes the call.
|
|
750
754
|
|
|
751
755
|
```ts
|
|
752
756
|
defineTool({
|
|
@@ -1861,6 +1865,7 @@ The factory gets the session's `SessionContext`, fixed when the session is made:
|
|
|
1861
1865
|
- `addressee` is whom tool descriptions should name: the primary owner exactly as configured in their own private conversation, another person by their name and pronouns (their name alone when they gave none), and `THE_SPEAKER` in a shared conversation, or a private one of someone the host knows by no name yet.
|
|
1862
1866
|
- `memory` is `"none"` for a persona that declares it, and `"speaker"` otherwise; a memory tool of your own loads nothing for `"none"`. A task the session runs with `runTask` keeps its conversation's `memory`.
|
|
1863
1867
|
- `speaker()` is the running turn's speaker, undefined between turns; a tool reads it when it runs, never when the session is made, since a session outlives the turn it is built in.
|
|
1868
|
+
- `origin()` is where the running turn's request came from, as `TurnRequest.origin` says, and undefined between turns and for a turn whose transport gives none; read it when the tool runs, like `speaker()`. It is optional on the type so a context built before 0.9.10 still type-checks.
|
|
1864
1869
|
|
|
1865
1870
|
The core's Jev compactor is one such extension:
|
|
1866
1871
|
|
|
@@ -2012,7 +2017,7 @@ Call `context.turns.run(input)` inside your claim's queue task to run a turn in
|
|
|
2012
2017
|
It shows typing and the stop control on the channel's surface, runs the turn, and emits `turnStarted` and `turnEnded` with the turn's `kind`.
|
|
2013
2018
|
It catches runtime errors as failed results and posts the answer, failure notice, or stopped notice through the surface.
|
|
2014
2019
|
Supply `reply(result)` to handle the reply yourself.
|
|
2015
|
-
`input` has `channel`, `kind`, `text`, `speaker`, and optionally `attachments`, `selection` (by default the plugins' `agentSelection`), `steerable`, `interactive`, `confirmed`, `reply`, and `conversation`.
|
|
2020
|
+
`input` has `channel`, `kind`, `text`, `speaker`, and optionally `origin` (where the request came from, as your claim's transport knows it, such as a connection's browser origin; tools read it as `turn.origin`, so never fill it from message text), `attachments`, `selection` (by default the plugins' `agentSelection`), `steerable`, `interactive`, `confirmed`, `reply`, and `conversation`.
|
|
2016
2021
|
It rejects with `NotLinkedError` during `setup`, and with a `PluginError` on a host whose runtime plugin has not provided a runtime.
|
|
2017
2022
|
|
|
2018
2023
|
Before each turn runs, `context.turns.run` records its conversation in the host's registry, `CONVERSATIONS`: at the first turn its key, surface, kind, visibility, owner, and title, and at every later turn only that it was active.
|
|
@@ -2955,7 +2960,7 @@ It returns:
|
|
|
2955
2960
|
| `contribution` | What the plugin added, as the host would collect it: `tools`, `prompt`, `seeds`, `events`, `services`, `http`, and the rest |
|
|
2956
2961
|
| `tools`, `tiers` | The tool names, and the table that says what tier each needs |
|
|
2957
2962
|
| `holds` | The plugin's `holdRules` chained as the host links them (`holdChain`): `holds(tool, input, { workspace?, scratchDir? })` returns the description of a call that must be approved first, or `undefined`; `holds.inTurn(tool, input, context, turn)` asks them as a turn does, and returns a promise when a hold looks things up |
|
|
2958
|
-
| `runTool(name, args, { speaker, channel, workspace }?)` | Runs a tool the way an agent's turn would, in the channel (default `test:1`) for the speaker, and returns the text the model reads; `workspace` (`{ workspace, scratchDir? }`) is the session's `turn.workspace` |
|
|
2963
|
+
| `runTool(name, args, { speaker, origin, channel, workspace }?)` | Runs a tool the way an agent's turn would, in the channel (default `test:1`) for the speaker, and returns the text the model reads; `origin` is the turn's `turn.origin`, and `workspace` (`{ workspace, scratchDir? }`) is the session's `turn.workspace` |
|
|
2959
2964
|
| `files` | Accepted files from `runTool`, recorded as `{ channel, file: ReplyFile }`; inject a surface with `supportsFiles: true` and pass its channel to test attachment tools |
|
|
2960
2965
|
| `events` | The events the plugin itself reported through `context.events`, and those of `context.turns` |
|
|
2961
2966
|
| `conversations`, `turns`, `surfaces` | What the plugin sees as `context.conversations`, `context.turns`, and `context.surfaces`, for a test to drive its claims |
|
package/package.json
CHANGED
package/src/core/define.ts
CHANGED
|
@@ -26,6 +26,14 @@ export interface ToolTurn {
|
|
|
26
26
|
/** The agent whose turn it is. */
|
|
27
27
|
agent: AgentTurnScope | undefined;
|
|
28
28
|
signal: AbortSignal | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Where the turn's request came from, as its transport reported it, such as the browser origin
|
|
31
|
+
* of a web chat connection (`https://chat.example.com`); undefined for a turn whose channel gives
|
|
32
|
+
* none, such as a Discord message or a schedule. Only the transport sets it, never the message
|
|
33
|
+
* text or the model, so a tool may label a record with it; a tool that needs one fails closed
|
|
34
|
+
* when it is undefined.
|
|
35
|
+
*/
|
|
36
|
+
origin?: string;
|
|
29
37
|
/**
|
|
30
38
|
* The session's shared workspace and scratch dir, where a tool may save a file for the model to
|
|
31
39
|
* pass on by path; undefined for a session without a workspace.
|
package/src/core/domain/ports.ts
CHANGED
|
@@ -30,6 +30,13 @@ export interface TurnRequest {
|
|
|
30
30
|
* starts a turn on someone's behalf gets theirs from `IDENTITY.speakerFor`.
|
|
31
31
|
*/
|
|
32
32
|
speaker: Speaker;
|
|
33
|
+
/**
|
|
34
|
+
* Where the request came from, as the transport that carried it knows: the browser origin of a
|
|
35
|
+
* web chat connection, such as `https://chat.example.com`. A channel with no such thing leaves it
|
|
36
|
+
* out. Only a channel's transport sets it, never the message text or the model; a tool reads it
|
|
37
|
+
* as `ToolTurn.origin`.
|
|
38
|
+
*/
|
|
39
|
+
origin?: string;
|
|
33
40
|
/**
|
|
34
41
|
* Who the conversation belongs to, as the host records it: `private` to one principal, or
|
|
35
42
|
* `shared` by whoever its claim admits, which it is taken to be when absent. A private
|
|
@@ -29,6 +29,12 @@ export interface ConversationTurnInput {
|
|
|
29
29
|
text: string;
|
|
30
30
|
/** Who the turn is for; its tier limits the tools. */
|
|
31
31
|
speaker: Speaker;
|
|
32
|
+
/**
|
|
33
|
+
* Where the request came from, as the claim's transport knows it, such as a web chat
|
|
34
|
+
* connection's browser origin; tools read it as `ToolTurn.origin`. Leave it out for a turn the
|
|
35
|
+
* transport gives none, and never fill it from message text.
|
|
36
|
+
*/
|
|
37
|
+
origin?: string;
|
|
32
38
|
/** Files and images the message carries. */
|
|
33
39
|
attachments?: TurnAttachments;
|
|
34
40
|
/** The turn's tools; by default the plugins' `agentSelection`, read at the turn. */
|
|
@@ -184,6 +190,7 @@ export function conversationTurns(
|
|
|
184
190
|
},
|
|
185
191
|
text: input.text,
|
|
186
192
|
speaker,
|
|
193
|
+
...(input.origin === undefined ? {} : { origin: input.origin }),
|
|
187
194
|
...(conversation ? { conversation } : {}),
|
|
188
195
|
...(input.attachments
|
|
189
196
|
? { attachments: input.attachments }
|
|
@@ -71,10 +71,10 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
71
71
|
readonly #tiers: ToolTiers;
|
|
72
72
|
readonly #factory: SessionFactory;
|
|
73
73
|
readonly #sessions: ConversationSessions;
|
|
74
|
-
/** The tier and
|
|
74
|
+
/** The tier, speaker and origin of each conversation's running turn. */
|
|
75
75
|
readonly #turns = new Map<
|
|
76
76
|
ChannelKey,
|
|
77
|
-
{ tier: Tier; speaker: Speaker; withheld: boolean }
|
|
77
|
+
{ tier: Tier; speaker: Speaker; origin?: string; withheld: boolean }
|
|
78
78
|
>();
|
|
79
79
|
/** Each conversation's turn in progress, which the owner may steer or stop. */
|
|
80
80
|
readonly #running = new Map<ChannelKey, SteerableRun>();
|
|
@@ -87,6 +87,7 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
87
87
|
this.#tiers = options.toolTiers ?? toolTiers();
|
|
88
88
|
this.#factory = new SessionFactory(options, {
|
|
89
89
|
speaker: (channel) => this.#turns.get(channel)?.speaker,
|
|
90
|
+
origin: (channel) => this.#turns.get(channel)?.origin,
|
|
90
91
|
withholdsMemory: (channel) => this.#turns.get(channel)?.withheld === true,
|
|
91
92
|
runTask: (scope, task) => this.#runTask(scope, task),
|
|
92
93
|
});
|
|
@@ -192,7 +193,12 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
192
193
|
loadsMemory(registered),
|
|
193
194
|
);
|
|
194
195
|
this.#memoryTurns.set(key, markPrivateMemory);
|
|
195
|
-
this.#turns.set(key, {
|
|
196
|
+
this.#turns.set(key, {
|
|
197
|
+
tier,
|
|
198
|
+
speaker: request.speaker,
|
|
199
|
+
...(request.origin === undefined ? {} : { origin: request.origin }),
|
|
200
|
+
withheld,
|
|
201
|
+
});
|
|
196
202
|
const missing = wanted.filter((name) => !registered.has(name));
|
|
197
203
|
if (missing.length > 0) {
|
|
198
204
|
logger.warn(
|
|
@@ -73,6 +73,8 @@ import {
|
|
|
73
73
|
export interface SessionFactoryDeps {
|
|
74
74
|
/** The person the conversation's running turn is for. */
|
|
75
75
|
speaker(channel: ChannelKey): Speaker | undefined;
|
|
76
|
+
/** Where the conversation's running turn came from, as its transport reported it. */
|
|
77
|
+
origin(channel: ChannelKey): string | undefined;
|
|
76
78
|
/** Whether the conversation's running turn loads no memory, as the agent server's option keeps a speaker out of it. */
|
|
77
79
|
withholdsMemory(channel: ChannelKey): boolean;
|
|
78
80
|
/**
|
|
@@ -355,6 +357,7 @@ export class SessionFactory {
|
|
|
355
357
|
return state.memory;
|
|
356
358
|
},
|
|
357
359
|
speaker: () => this.#deps.speaker(turnKey),
|
|
360
|
+
origin: () => this.#deps.origin(turnKey),
|
|
358
361
|
runTask: (task) =>
|
|
359
362
|
this.#deps.runTask({ turn: turnKey, home: channel }, task),
|
|
360
363
|
};
|
package/src/core/sessions.ts
CHANGED
|
@@ -92,6 +92,12 @@ export interface SessionContext {
|
|
|
92
92
|
attachmentDir?: string;
|
|
93
93
|
/** The person the session's running turn is for; undefined between turns. */
|
|
94
94
|
speaker(): Speaker | undefined;
|
|
95
|
+
/**
|
|
96
|
+
* Where the running turn's request came from, as `TurnRequest.origin` says; undefined between
|
|
97
|
+
* turns and for a turn whose transport gives none. A context built before 0.9.10 has no such
|
|
98
|
+
* method, which reads as undefined.
|
|
99
|
+
*/
|
|
100
|
+
origin?(): string | undefined;
|
|
95
101
|
/** Runs a task beside this session, under its confirmation gate. */
|
|
96
102
|
runTask(task: TransientTask): Promise<string>;
|
|
97
103
|
}
|
package/src/core/tool-turn.ts
CHANGED
|
@@ -15,11 +15,13 @@ export function toolTurn(
|
|
|
15
15
|
context: SessionContext,
|
|
16
16
|
signal: AbortSignal | undefined,
|
|
17
17
|
): ToolTurn {
|
|
18
|
+
const origin = context.origin?.();
|
|
18
19
|
return {
|
|
19
20
|
speaker: context.speaker(),
|
|
20
21
|
channel: context.turnChannel,
|
|
21
22
|
agent: context.agent,
|
|
22
23
|
signal,
|
|
24
|
+
...(origin === undefined ? {} : { origin }),
|
|
23
25
|
...(context.workspace ? { workspace: context.workspace } : {}),
|
|
24
26
|
attachFile: attachReplyFile,
|
|
25
27
|
attachment: async (file) => {
|
package/src/testing.ts
CHANGED
|
@@ -270,6 +270,8 @@ export interface TestPluginResult {
|
|
|
270
270
|
/** `workspace` gives the session a workspace and scratch dir, as an agent's session has. */
|
|
271
271
|
options?: {
|
|
272
272
|
speaker?: Speaker;
|
|
273
|
+
/** Where the turn's request came from, as `ToolTurn.origin` shows it to the tool. */
|
|
274
|
+
origin?: string;
|
|
273
275
|
channel?: ChannelKey;
|
|
274
276
|
workspace?: SessionContext["workspace"];
|
|
275
277
|
},
|
|
@@ -640,6 +642,7 @@ export async function testPlugin(
|
|
|
640
642
|
...(runOptions?.workspace ? { workspace: runOptions.workspace } : {}),
|
|
641
643
|
attachmentDir: ownerAttachmentDir(attachmentDir, channel),
|
|
642
644
|
speaker: () => runOptions?.speaker,
|
|
645
|
+
origin: () => runOptions?.origin,
|
|
643
646
|
runTask: async () => {
|
|
644
647
|
throw new Error("test session cannot run tasks");
|
|
645
648
|
},
|