@omercnet/paseo-tell-agent 0.3.0 → 1.0.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/README.md +15 -17
- package/client/agents.ts +158 -3
- package/client/message-agent.tsx +12 -13
- package/package.json +6 -6
- package/paseo-plugin.json +2 -1
package/README.md
CHANGED
|
@@ -2,39 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
Send a message from one Paseo agent to another agent or workspace on the same daemon host.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Screenshots
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Agent and workspace names are synthetic. Both PNGs were captured from an isolated Paseo test
|
|
8
|
+
daemon at 2× pixel density after browser DevTools verified that no private organization names
|
|
9
|
+
remained in the rendered page.
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
### Choose a target
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
paseo plugin install npm:@omercnet/paseo-tell-agent
|
|
13
|
-
```
|
|
13
|
+

|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
### Compose an instruction
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
paseo plugin update tell-agent
|
|
19
|
-
```
|
|
17
|
+

|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
## Install
|
|
22
20
|
|
|
23
|
-
### Paseo 0.
|
|
21
|
+
### Paseo 0.9.0 or later
|
|
24
22
|
|
|
25
|
-
|
|
23
|
+
Install the npm package:
|
|
26
24
|
|
|
27
25
|
```bash
|
|
28
|
-
paseo plugin
|
|
26
|
+
paseo plugin install npm:@omercnet/paseo-tell-agent
|
|
29
27
|
```
|
|
30
28
|
|
|
31
|
-
|
|
29
|
+
Update an npm installation by its plugin ID:
|
|
32
30
|
|
|
33
31
|
```bash
|
|
34
|
-
paseo plugin
|
|
32
|
+
paseo plugin update tell-agent
|
|
35
33
|
```
|
|
36
34
|
|
|
37
|
-
|
|
35
|
+
Paseo 0.9's reviewed update flow shows the current and proposed revisions before asking for approval. Add `--check` to preview without applying or `--yes` to skip the approval prompt.
|
|
38
36
|
|
|
39
37
|
## Use
|
|
40
38
|
|
package/client/agents.ts
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PluginClientContext } from "@getpaseo/plugin/client";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
type
|
|
3
|
+
type PaseoApi = PluginClientContext["paseo"];
|
|
4
|
+
type PaseoAgentUpdate = Parameters<Parameters<PaseoApi["agents"]["subscribe"]>[0]>[0];
|
|
5
|
+
type AgentSnapshot = Extract<PaseoAgentUpdate, { kind: "upsert" }>["agent"];
|
|
6
|
+
export type AgentEntry = {
|
|
7
|
+
agent: AgentSnapshot;
|
|
8
|
+
project: {
|
|
9
|
+
projectName: string;
|
|
10
|
+
workspaceName?: string | null;
|
|
11
|
+
checkout: { isGit: boolean; currentBranch: string | null };
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
type PaseoAgentListResult = {
|
|
15
|
+
entries: AgentEntry[];
|
|
16
|
+
pageInfo: { hasMore: boolean; nextCursor: string | null };
|
|
17
|
+
};
|
|
5
18
|
|
|
6
19
|
export const AGENT_PAGE_LIMIT = 200;
|
|
7
20
|
export const MAX_AGENT_PAGES = 10;
|
|
21
|
+
const REOPEN_MIN_MS = 2_000;
|
|
22
|
+
const REOPEN_MAX_MS = 60_000;
|
|
8
23
|
|
|
9
24
|
export type AgentDirectoryPage = {
|
|
10
25
|
entries: AgentEntry[];
|
|
@@ -26,6 +41,146 @@ export async function loadAgents(paseo: PaseoApi): Promise<AgentDirectoryPage> {
|
|
|
26
41
|
return { entries, truncated: true };
|
|
27
42
|
}
|
|
28
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Continues `loadAgents` from an observation's first page, to the same cap.
|
|
46
|
+
* Returns null once `current()` turns false, so a stale read is dropped.
|
|
47
|
+
*/
|
|
48
|
+
async function readRemainingPages(
|
|
49
|
+
paseo: PaseoApi,
|
|
50
|
+
first: PaseoAgentListResult,
|
|
51
|
+
signal: AbortSignal,
|
|
52
|
+
current: () => boolean,
|
|
53
|
+
): Promise<AgentDirectoryPage | null> {
|
|
54
|
+
const entries = [...first.entries];
|
|
55
|
+
let cursor = first.pageInfo.hasMore ? (first.pageInfo.nextCursor ?? undefined) : undefined;
|
|
56
|
+
for (let page = 1; cursor; page += 1) {
|
|
57
|
+
if (page >= MAX_AGENT_PAGES) return { entries, truncated: true };
|
|
58
|
+
const result = await paseo.agents.list({
|
|
59
|
+
sort: [{ key: "updated_at", direction: "desc" }],
|
|
60
|
+
page: { limit: AGENT_PAGE_LIMIT, cursor },
|
|
61
|
+
signal,
|
|
62
|
+
});
|
|
63
|
+
if (!current()) return null;
|
|
64
|
+
entries.push(...result.entries);
|
|
65
|
+
cursor = result.pageInfo.hasMore ? (result.pageInfo.nextCursor ?? undefined) : undefined;
|
|
66
|
+
}
|
|
67
|
+
return { entries, truncated: false };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type AgentDirectoryFollower = {
|
|
71
|
+
/**
|
|
72
|
+
* The directory as listed. `complete` means it holds every agent the host
|
|
73
|
+
* has, so anything missing is gone; otherwise apply it as upserts only.
|
|
74
|
+
*/
|
|
75
|
+
snapshot(agents: AgentSnapshot[], complete: boolean): void;
|
|
76
|
+
upsert(agent: AgentSnapshot): void;
|
|
77
|
+
remove(agentId: string): void;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Keeps `follower` in step with the host's agents until the returned cleanup.
|
|
82
|
+
*
|
|
83
|
+
* `agents.subscribe()` only hears observations the same API instance opened,
|
|
84
|
+
* so this opens one with `list({ subscribe: {} })`. Its snapshot, the first
|
|
85
|
+
* page, arrives first and again after every reconnect; later pages are read
|
|
86
|
+
* plainly, and updates that land meanwhile win over what those pages say.
|
|
87
|
+
* Paseo releases an observation that fails, so it is reopened with backoff.
|
|
88
|
+
*/
|
|
89
|
+
export function followAgentDirectory(
|
|
90
|
+
paseo: PaseoApi,
|
|
91
|
+
follower: AgentDirectoryFollower,
|
|
92
|
+
): () => void {
|
|
93
|
+
let stopped = false;
|
|
94
|
+
const apply = (update: PaseoAgentUpdate) => {
|
|
95
|
+
if (update.kind === "remove") follower.remove(update.agentId);
|
|
96
|
+
else follower.upsert(update.agent);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const lifetime = new AbortController();
|
|
100
|
+
let observation: { release(): Promise<void> } | null = null;
|
|
101
|
+
let reopenTimer: ReturnType<typeof setTimeout> | null = null;
|
|
102
|
+
let reopenDelay = REOPEN_MIN_MS;
|
|
103
|
+
/** Bumped per snapshot, so a reconnect abandons the paging of the one before. */
|
|
104
|
+
let generation = 0;
|
|
105
|
+
/** What updates said while the current snapshot's later pages were read. */
|
|
106
|
+
let landed: Map<string, AgentSnapshot | null> | null = null;
|
|
107
|
+
|
|
108
|
+
const applySnapshot = async (first: PaseoAgentListResult) => {
|
|
109
|
+
const current = ++generation;
|
|
110
|
+
const isCurrent = () => !stopped && generation === current;
|
|
111
|
+
const touched = new Map<string, AgentSnapshot | null>();
|
|
112
|
+
landed = touched;
|
|
113
|
+
let listed: AgentDirectoryPage | null;
|
|
114
|
+
try {
|
|
115
|
+
listed = await readRemainingPages(paseo, first, lifetime.signal, isCurrent);
|
|
116
|
+
} catch {
|
|
117
|
+
listed = { entries: [...first.entries], truncated: true };
|
|
118
|
+
}
|
|
119
|
+
if (!listed || !isCurrent()) return;
|
|
120
|
+
landed = null;
|
|
121
|
+
const agents = new Map(listed.entries.map((entry) => [entry.agent.id, entry.agent]));
|
|
122
|
+
for (const [agentId, agent] of touched) {
|
|
123
|
+
if (agent) agents.set(agentId, agent);
|
|
124
|
+
else agents.delete(agentId);
|
|
125
|
+
}
|
|
126
|
+
follower.snapshot([...agents.values()], !listed.truncated);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const reopen = () => {
|
|
130
|
+
observation = null;
|
|
131
|
+
generation += 1;
|
|
132
|
+
landed = null;
|
|
133
|
+
if (stopped || reopenTimer !== null) return;
|
|
134
|
+
reopenTimer = setTimeout(() => {
|
|
135
|
+
reopenTimer = null;
|
|
136
|
+
open();
|
|
137
|
+
}, reopenDelay);
|
|
138
|
+
reopenDelay = Math.min(reopenDelay * 2, REOPEN_MAX_MS);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const open = () => {
|
|
142
|
+
paseo.agents
|
|
143
|
+
.list({
|
|
144
|
+
sort: [{ key: "updated_at", direction: "desc" }],
|
|
145
|
+
page: { limit: AGENT_PAGE_LIMIT },
|
|
146
|
+
subscribe: {},
|
|
147
|
+
signal: lifetime.signal,
|
|
148
|
+
})
|
|
149
|
+
.then(({ subscription }) => {
|
|
150
|
+
if (stopped) {
|
|
151
|
+
void subscription.release().catch(() => undefined);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
observation = subscription;
|
|
155
|
+
subscription.subscribe({
|
|
156
|
+
snapshot: (first) => {
|
|
157
|
+
reopenDelay = REOPEN_MIN_MS;
|
|
158
|
+
void applySnapshot(first);
|
|
159
|
+
},
|
|
160
|
+
update: (message) => {
|
|
161
|
+
if (stopped || message.type !== "agent_update") return;
|
|
162
|
+
const update = message.payload;
|
|
163
|
+
if (update.kind === "remove") landed?.set(update.agentId, null);
|
|
164
|
+
else landed?.set(update.agent.id, update.agent);
|
|
165
|
+
apply(update);
|
|
166
|
+
},
|
|
167
|
+
error: reopen,
|
|
168
|
+
});
|
|
169
|
+
})
|
|
170
|
+
.catch(reopen);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
open();
|
|
174
|
+
return () => {
|
|
175
|
+
stopped = true;
|
|
176
|
+
lifetime.abort();
|
|
177
|
+
if (reopenTimer !== null) clearTimeout(reopenTimer);
|
|
178
|
+
reopenTimer = null;
|
|
179
|
+
void observation?.release().catch(() => undefined);
|
|
180
|
+
observation = null;
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
29
184
|
export function title(entry: AgentEntry): string {
|
|
30
185
|
const explicit = entry.agent.title?.trim();
|
|
31
186
|
return explicit && explicit.length > 0 ? explicit : entry.agent.id.slice(0, 7);
|
package/client/message-agent.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import { Pressable, ScrollView, Text, TextInput, View } from "react-native";
|
|
|
11
11
|
import {
|
|
12
12
|
AGENT_PAGE_LIMIT,
|
|
13
13
|
type AgentEntry,
|
|
14
|
+
followAgentDirectory,
|
|
14
15
|
loadAgents,
|
|
15
16
|
MAX_AGENT_PAGES,
|
|
16
17
|
placement,
|
|
@@ -328,20 +329,18 @@ export function contributeAgentMessaging(client: PluginClientContext) {
|
|
|
328
329
|
pills.set(agent.id, { workspaceId: next.workspaceId, remove: pill.remove });
|
|
329
330
|
}
|
|
330
331
|
|
|
331
|
-
const unsubscribe = client.paseo
|
|
332
|
-
|
|
333
|
-
removePill(update.agentId);
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
syncAgent(update.agent);
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
void loadAgents(client.paseo)
|
|
340
|
-
.then(({ entries }) => {
|
|
332
|
+
const unsubscribe = followAgentDirectory(client.paseo, {
|
|
333
|
+
snapshot(agents, complete) {
|
|
341
334
|
if (stopped) return;
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
335
|
+
if (complete) {
|
|
336
|
+
const listed = new Set(agents.map((agent) => agent.id));
|
|
337
|
+
for (const agentId of [...pills.keys()]) if (!listed.has(agentId)) removePill(agentId);
|
|
338
|
+
}
|
|
339
|
+
for (const agent of agents) syncAgent(agent);
|
|
340
|
+
},
|
|
341
|
+
upsert: syncAgent,
|
|
342
|
+
remove: removePill,
|
|
343
|
+
});
|
|
345
344
|
|
|
346
345
|
const removeTellCommand = client.addSlashCommand({
|
|
347
346
|
name: "tell",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omercnet/paseo-tell-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Send messages to agents in other workspaces on the same Paseo host.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"publishConfig": {
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
|
-
"packageManager": "
|
|
19
|
+
"packageManager": "bun@1.4.2",
|
|
20
20
|
"engines": {
|
|
21
21
|
"node": ">=24"
|
|
22
22
|
},
|
|
@@ -43,11 +43,11 @@
|
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@biomejs/biome": "^2.5.10",
|
|
46
|
-
"@getpaseo/client": "0.9.0
|
|
47
|
-
"@getpaseo/plugin": "0.9.0
|
|
48
|
-
"@getpaseo/protocol": "0.9.0
|
|
46
|
+
"@getpaseo/client": "0.9.0",
|
|
47
|
+
"@getpaseo/plugin": "0.9.0",
|
|
48
|
+
"@getpaseo/protocol": "0.9.0",
|
|
49
49
|
"@tanstack/react-query": "^5.102.3",
|
|
50
|
-
"@types/node": "^24.
|
|
50
|
+
"@types/node": "^24.10.1",
|
|
51
51
|
"@types/react": "^19.2.18",
|
|
52
52
|
"react": "19.1.0",
|
|
53
53
|
"react-native": "0.81.5",
|
package/paseo-plugin.json
CHANGED