@khorsheed/dsh-room 0.2.2 → 0.2.4
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 +58 -0
- package/README.en.md +29 -62
- package/README.i18n.yaml +2 -2
- package/README.md +28 -61
- package/lib/client.js +14620 -5705
- package/lib/index.js +4069 -730
- package/lib/journal-B4FOGO6S.js +482 -0
- package/lib/journal-Rid9AAnD.js +485 -0
- package/lib/tool.js +112 -166
- package/lib/typert.host.js +2031 -374
- package/lib/typert.remote-client.d.ts +34 -10
- package/lib/typert.remote-client.js +1599 -210
- package/lib/types/agent-setup.d.ts +1 -1
- package/lib/types/client/InviteAgentAction.js +4 -16
- package/lib/types/client/InviteDialog.d.ts +28 -6
- package/lib/types/client/InviteDialog.js +76 -12
- package/lib/types/client/MemberCard.d.ts +5 -1
- package/lib/types/client/MemberCard.js +4 -4
- package/lib/types/client/MembersView.d.ts +1 -1
- package/lib/types/client/MembersView.js +58 -22
- package/lib/types/client/RemoveMemberConfirm.d.ts +40 -0
- package/lib/types/client/RemoveMemberConfirm.js +85 -0
- package/lib/types/client/RoomApprovalView.d.ts +11 -0
- package/lib/types/client/RoomApprovalView.js +28 -0
- package/lib/types/client/RoomComposer.d.ts +1 -1
- package/lib/types/client/RoomComposer.js +195 -29
- package/lib/types/client/RoomDecisionView.d.ts +22 -0
- package/lib/types/client/RoomDecisionView.js +35 -0
- package/lib/types/client/RoomHelpView.d.ts +17 -0
- package/lib/types/client/RoomHelpView.js +56 -0
- package/lib/types/client/RoomInboxView.d.ts +10 -0
- package/lib/types/client/RoomInboxView.js +14 -0
- package/lib/types/client/RoomQuestionView.d.ts +13 -0
- package/lib/types/client/RoomQuestionView.js +61 -0
- package/lib/types/client/RoomRelayView.js +1 -1
- package/lib/types/client/RoomRunView.d.ts +3 -1
- package/lib/types/client/RoomRunView.js +12 -5
- package/lib/types/client/RoomSettingsView.d.ts +19 -0
- package/lib/types/client/RoomSettingsView.js +30 -0
- package/lib/types/client/RoomSpeechView.d.ts +22 -8
- package/lib/types/client/RoomSpeechView.js +94 -11
- package/lib/types/client/RoomStatsLine.d.ts +26 -5
- package/lib/types/client/RoomStatsLine.js +53 -7
- package/lib/types/client/RoomTaskCapsule.d.ts +19 -0
- package/lib/types/client/RoomTaskCapsule.js +37 -0
- package/lib/types/client/TasksTab.d.ts +41 -0
- package/lib/types/client/TasksTab.js +43 -0
- package/lib/types/client/TasksView.d.ts +44 -0
- package/lib/types/client/TasksView.js +149 -0
- package/lib/types/client/TeamView.d.ts +33 -0
- package/lib/types/client/TeamView.js +210 -0
- package/lib/types/client/at-menu.d.ts +127 -0
- package/lib/types/client/at-menu.js +173 -0
- package/lib/types/client/draft-chips.d.ts +67 -0
- package/lib/types/client/draft-chips.js +95 -0
- package/lib/types/client/executions.d.ts +0 -30
- package/lib/types/client/executions.js +4 -34
- package/lib/types/client/icons-local.d.ts +25 -0
- package/lib/types/client/icons-local.js +10 -0
- package/lib/types/client/icons.d.ts +2 -1
- package/lib/types/client/icons.js +1 -0
- package/lib/types/client/index.js +276 -72
- package/lib/types/client/locales.d.ts +482 -52
- package/lib/types/client/locales.js +490 -60
- package/lib/types/client/nodes.d.ts +112 -17
- package/lib/types/client/nodes.js +197 -26
- package/lib/types/client/slots.d.ts +117 -47
- package/lib/types/client/task-tag.d.ts +32 -0
- package/lib/types/client/task-tag.js +33 -0
- package/lib/types/client/tasks.d.ts +50 -0
- package/lib/types/client/tasks.js +82 -0
- package/lib/types/client/team.d.ts +73 -0
- package/lib/types/client/team.js +187 -0
- package/lib/types/decisions.d.ts +49 -0
- package/lib/types/decisions.js +101 -0
- package/lib/types/deliveries.d.ts +17 -1
- package/lib/types/deliveries.js +82 -6
- package/lib/types/dispatch.d.ts +102 -4
- package/lib/types/dispatch.js +498 -109
- package/lib/types/help.d.ts +137 -0
- package/lib/types/help.js +216 -0
- package/lib/types/helps.d.ts +26 -0
- package/lib/types/helps.js +47 -0
- package/lib/types/index.d.ts +292 -52
- package/lib/types/index.js +1421 -199
- package/lib/types/jobs.d.ts +22 -0
- package/lib/types/jobs.js +77 -0
- package/lib/types/journal.d.ts +17 -27
- package/lib/types/journal.js +120 -43
- package/lib/types/messaging.d.ts +65 -0
- package/lib/types/messaging.js +160 -0
- package/lib/types/plan-service.js +7 -1
- package/lib/types/report-gate.d.ts +81 -0
- package/lib/types/report-gate.js +89 -0
- package/lib/types/tool-definition.d.ts +43 -20
- package/lib/types/tool-definition.js +145 -212
- package/lib/types/tool.d.ts +2 -2
- package/lib/types/tool.js +3 -2
- package/lib/types/types.d.ts +755 -78
- package/locale/zh.json +1 -1
- package/package.json +3 -3
- package/lib/types/client/RoomActivityDock.d.ts +0 -19
- package/lib/types/client/RoomActivityDock.js +0 -105
- package/lib/types/client/RoomDockCapsules.d.ts +0 -64
- package/lib/types/client/RoomDockCapsules.js +0 -277
- package/lib/types/client/RoomPlanTab.d.ts +0 -15
- package/lib/types/client/RoomPlanTab.js +0 -13
- package/lib/types/client/RoomTaskLineView.d.ts +0 -13
- package/lib/types/client/RoomTaskLineView.js +0 -21
|
@@ -1,35 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The room's model-facing tool DEFINITIONS, `room_invite`, `room_task`, and
|
|
3
|
-
* `room_message`: the main
|
|
4
|
-
* agent's paths into the room it belongs to. `room_invite` and
|
|
5
|
-
* `room_message` PROMOTE: calling them in a plain session turns it into a
|
|
6
|
-
* room (ensureRoom) instead of rejecting. `room_task` stays gated at execute
|
|
7
|
-
* time — the calling agent's session must be a room (the journal carries
|
|
8
|
-
* `room/created`); anywhere else the tool answers with readable error text
|
|
9
|
-
* instead of throwing, so the model can see the rejection and self-correct.
|
|
10
|
-
* `room_invite` lands in the same invite path as the human dialog
|
|
11
|
-
* (`invitedBy: 'agent'`), producing an identical member record; `room_task`
|
|
12
|
-
* writes the SHARED task board through the very host functions the capsule UI
|
|
13
|
-
* uses (addTask/closeTask, plus the tool-only updateTask), so an agent-written
|
|
14
|
-
* task is indistinguishable from a human-added one; `room_message` dispatches
|
|
15
|
-
* a message to a member through the same engine path as the human's
|
|
16
|
-
* `@member text`, minus the user bubble (the caller is the room's own agent).
|
|
17
|
-
*
|
|
18
|
-
* Since the tool-row split (M4') the core never REGISTERS these tools: the
|
|
19
|
-
* factories are consumed by the package's `./tool` subpath row
|
|
20
|
-
* ({@link file://./tool.ts}), which mounts them inside agent-preset
|
|
21
|
-
* compositions (session-granted, never at the profile root). The definitions
|
|
22
|
-
* are therefore returned UNTAGGED — the registering row applies its own
|
|
23
|
-
* tool-origin tag (AGENTS.md § Tool origin tagging — the owner is whichever
|
|
24
|
-
* package mounts the row).
|
|
25
|
-
*
|
|
26
|
-
* Internal module: not on the package's exports map (consumers reach it only
|
|
27
|
-
* through `./src/*`).
|
|
28
|
-
*
|
|
29
|
-
* @module @khorsheed/dsh-room
|
|
30
|
-
*/
|
|
31
1
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
32
|
-
import { coordinatorMember, isRoomLog } from './journal.js';
|
|
33
2
|
/**
|
|
34
3
|
* Build the room_invite tool definition bound to the room service.
|
|
35
4
|
* @param backend - the room service.
|
|
@@ -44,41 +13,56 @@ export function roomInviteTool(backend) {
|
|
|
44
13
|
+ 'name (no whitespace, no "@", e.g. ada/bill/cathy) — it is the addressing name, decoupled '
|
|
45
14
|
+ 'from the provider so two instances of one provider can coexist; prefer a function-style '
|
|
46
15
|
+ 'name (e.g. design-review / restart-test) when the member has a standing role, so the human '
|
|
47
|
-
+ 'and the other members can tell what it is for.
|
|
48
|
-
+ '
|
|
16
|
+
+ 'and the other members can tell what it is for. Give a short public role (what everyone '
|
|
17
|
+
+ 'else sees) and private instructions: the instructions are the member\'s own briefing, '
|
|
18
|
+
+ 'prepended to its first dispatch and persisted in its own CLI '
|
|
49
19
|
+ 'session from then on. Give a firstTask to put the member to work immediately, or omit it '
|
|
50
20
|
+ 'to have the member join idle. A member keeps its own CLI session and accumulates context '
|
|
51
21
|
+ 'across rounds, so prefer inviting over a one-shot subagent_* delegation whenever the work '
|
|
52
22
|
+ 'expects follow-ups with the same agent — reserve one-shot delegations for self-contained '
|
|
53
|
-
+ 'tasks with no continuation.
|
|
54
|
-
+ '
|
|
23
|
+
+ 'tasks with no continuation. room_read lists the invitable providers and modes (it works before '
|
|
24
|
+
+ 'the session is a room too); the provider may be its id or the harness name (e.g. claude-code).',
|
|
55
25
|
parameters: {
|
|
56
26
|
provider: {
|
|
57
27
|
type: 'string',
|
|
58
28
|
required: true,
|
|
59
|
-
description: 'The
|
|
60
|
-
+ '
|
|
61
|
-
+ 'name. When unsure, call without firstTask first: an unknown id is rejected with the '
|
|
62
|
-
+ 'list of available providers to retry with.',
|
|
29
|
+
description: 'The CLI to invite: a provider id from room_read, or the harness name / display name '
|
|
30
|
+
+ '(claude-code, Claude Code, codex, kimi). An unknown one is rejected with the available ids.',
|
|
63
31
|
},
|
|
64
32
|
name: {
|
|
65
33
|
type: 'string',
|
|
66
34
|
required: true,
|
|
67
35
|
description: 'The member\'s @-addressing name: unique in this room, no whitespace, no "@".',
|
|
68
36
|
},
|
|
37
|
+
role: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
description: 'Short public role (≤30 chars, e.g. 代码审查 / 实现 / 测试): what the other members and the human see. Keep it a phrase; details go in instructions.',
|
|
40
|
+
},
|
|
69
41
|
instructions: {
|
|
70
42
|
type: 'string',
|
|
71
43
|
required: true,
|
|
72
|
-
description: 'The member\'s
|
|
44
|
+
description: 'The member\'s private instructions (responsibilities, constraints, output expectations). Only this member ever reads them.',
|
|
73
45
|
},
|
|
74
46
|
firstTask: {
|
|
75
47
|
type: 'string',
|
|
76
|
-
description: 'Optional first task, dispatched to the member as soon as it joins.',
|
|
48
|
+
description: 'Optional first task, dispatched to the member as soon as it joins. Its result is reported back to you automatically — do not sleep or poll room_read for it.',
|
|
49
|
+
},
|
|
50
|
+
note: {
|
|
51
|
+
type: 'string',
|
|
52
|
+
description: 'Optional: what you will wait for before acting on the first task\'s result (e.g. "wait until every invited member greeted"). The room uses it to decide when to hand you the report.',
|
|
77
53
|
},
|
|
78
54
|
cwd: {
|
|
79
55
|
type: 'string',
|
|
80
56
|
description: 'Optional working directory for the member (empty: inherits the room session\'s cwd).',
|
|
81
57
|
},
|
|
58
|
+
mode: {
|
|
59
|
+
type: 'string',
|
|
60
|
+
description: 'The member\'s dsh mode: a host agent preset id (e.g. dsh-gamedev, dsh-video). The member '
|
|
61
|
+
+ 'can then call that mode\'s plugin tools (and sees its skills) besides its own CLI tools. '
|
|
62
|
+
+ 'Omit it to keep this room\'s own mode — the usual choice; pass the mode the human asked for '
|
|
63
|
+
+ 'when they named one, or "none" for a member with only its own CLI tools. An unknown id is '
|
|
64
|
+
+ 'rejected with the available ids.',
|
|
65
|
+
},
|
|
82
66
|
},
|
|
83
67
|
output: {
|
|
84
68
|
schema: {
|
|
@@ -103,181 +87,41 @@ export function roomInviteTool(backend) {
|
|
|
103
87
|
provider: args.provider,
|
|
104
88
|
name: args.name,
|
|
105
89
|
instructions: args.instructions,
|
|
90
|
+
...args.role === undefined ? {} : { role: args.role },
|
|
106
91
|
...args.firstTask === undefined ? {} : { firstTask: args.firstTask },
|
|
92
|
+
...args.note === undefined ? {} : { note: args.note },
|
|
107
93
|
...args.cwd === undefined ? {} : { cwd: args.cwd },
|
|
94
|
+
...args.mode === undefined || args.mode.trim() === '' ? { inheritPreset: true } : args.mode.trim() === 'none' ? {} : { preset: args.mode.trim() },
|
|
108
95
|
}, 'agent');
|
|
109
96
|
if (!result.ok) {
|
|
110
97
|
const hint = result.error.code === 'duplicate-name'
|
|
111
98
|
? ' Pick a different name and retry.'
|
|
112
99
|
: result.error.code === 'invalid-name'
|
|
113
100
|
? ' Names must be non-empty and free of whitespace and "@"; retry with a valid name.'
|
|
114
|
-
: result.error.code === '
|
|
115
|
-
? ' The
|
|
116
|
-
: result.error.code === '
|
|
117
|
-
?
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
101
|
+
: result.error.code === 'role-too-long'
|
|
102
|
+
? ' The role must be at most 30 characters; shorten it and move details into instructions.'
|
|
103
|
+
: result.error.code === 'local-agent-unavailable'
|
|
104
|
+
? ' The local-agent delegation facade is not mounted; CLI members cannot join right now.'
|
|
105
|
+
: result.error.code === 'unknown-provider'
|
|
106
|
+
? ` Unknown delegation provider "${result.error.provider}" (it must be the family's delegation provider id, not the harness's display name). Available: ${result.error.available.length === 0
|
|
107
|
+
? 'none (no harness with a delegation provider is registered)'
|
|
108
|
+
: result.error.available.join(', ')}. Retry with one of the available providers.`
|
|
109
|
+
: result.error.code === 'unknown-preset'
|
|
110
|
+
? ` Unknown mode "${result.error.preset}". Available: ${result.error.available.join(', ') || 'none'}; or "none" for no mode.`
|
|
111
|
+
: '';
|
|
121
112
|
return { text: `Could not invite "${args.name}": ${result.error.code}.${hint}` };
|
|
122
113
|
}
|
|
123
114
|
return {
|
|
124
115
|
text: result.value.pendingFirstTask
|
|
125
|
-
? `Member ${result.value.name} joined the room and its first task was dispatched
|
|
126
|
-
: `Member ${result.value.name} joined the room (idle; @-address it to dispatch work)
|
|
116
|
+
? `Member ${result.value.name} joined the room and its first task was dispatched.${modeNote(result.value.preset)}`
|
|
117
|
+
: `Member ${result.value.name} joined the room (idle; @-address it to dispatch work).${modeNote(result.value.preset)}`,
|
|
127
118
|
};
|
|
128
119
|
},
|
|
129
120
|
});
|
|
130
121
|
}
|
|
131
|
-
/**
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
* opens is a public commitment indistinguishable from a human-added one.
|
|
135
|
-
* @param backend - the room service.
|
|
136
|
-
* @returns a registry-ready tool definition.
|
|
137
|
-
*/
|
|
138
|
-
export function roomTaskTool(backend) {
|
|
139
|
-
return defineTool({
|
|
140
|
-
name: 'room_task',
|
|
141
|
-
description: 'Manage the current room\'s SHARED task board (only usable inside a room session). This '
|
|
142
|
-
+ 'board is public — every room member and the human sees these tasks, so writing here is '
|
|
143
|
-
+ 'a public commitment the room coordinates around; for your private work plan use your own '
|
|
144
|
-
+ 'todo tool instead. Actions: "add" (title + member — the roster name owning the task, '
|
|
145
|
-
+ 'use room_read to find your own or another member\'s name; optional blockedBy names the member the task waits '
|
|
146
|
-
+ 'on, display only) opens a pending task and returns its id; "close" (taskId) marks an '
|
|
147
|
-
+ 'open task done; "update" (taskId + a new title and/or blockedBy — null clears the wait) '
|
|
148
|
-
+ 'edits an open task. Errors answer with the roster or the open-task list so you can retry '
|
|
149
|
-
+ 'with a valid member name or task id.',
|
|
150
|
-
parameters: {
|
|
151
|
-
action: {
|
|
152
|
-
type: 'string',
|
|
153
|
-
enum: ['add', 'close', 'update'],
|
|
154
|
-
required: true,
|
|
155
|
-
description: 'add: open a task (title + member). close: mark a task done (taskId). update: edit title/blockedBy (taskId).',
|
|
156
|
-
},
|
|
157
|
-
title: {
|
|
158
|
-
type: 'string',
|
|
159
|
-
description: 'Task title. Required for add; optional new title for update.',
|
|
160
|
-
},
|
|
161
|
-
member: {
|
|
162
|
-
type: 'string',
|
|
163
|
-
description: 'The roster name owning the task (required for add; read room_read for current names).',
|
|
164
|
-
},
|
|
165
|
-
taskId: {
|
|
166
|
-
type: 'string',
|
|
167
|
-
description: 'The task id (required for close/update; add\'s success text returns the new id).',
|
|
168
|
-
},
|
|
169
|
-
blockedBy: {
|
|
170
|
-
oneOf: [{ type: 'string' }, { type: 'null' }],
|
|
171
|
-
description: 'The member this task waits on (must be on the roster; display only, never an automatic '
|
|
172
|
-
+ 'dispatch). Optional on add; on update, a string re-targets the wait and null clears it.',
|
|
173
|
-
},
|
|
174
|
-
},
|
|
175
|
-
output: {
|
|
176
|
-
schema: {
|
|
177
|
-
type: 'object',
|
|
178
|
-
additionalProperties: false,
|
|
179
|
-
properties: {
|
|
180
|
-
text: { type: 'string', required: true },
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
render: (_args, value) => [{ type: 'text', text: value.text }],
|
|
184
|
-
},
|
|
185
|
-
// Board writes mutate only the room journal; two writes never conflict
|
|
186
|
-
// beyond the validations the result text reports back.
|
|
187
|
-
isConcurrencySafe: () => true,
|
|
188
|
-
async execute(args, exec) {
|
|
189
|
-
const agent = exec.agent;
|
|
190
|
-
if (agent === undefined)
|
|
191
|
-
return { text: 'room_task requires a calling agent (exec.agent was undefined).' };
|
|
192
|
-
if (!isRoomLog(agent.session.snapshotEvents())) {
|
|
193
|
-
return { text: 'The current session is not a room; room_task is only usable inside a room session.' };
|
|
194
|
-
}
|
|
195
|
-
const sessionId = agent.session.id;
|
|
196
|
-
const ownership = await backend.getState({ sessionId });
|
|
197
|
-
if (ownership.ok && coordinatorMember(ownership.value)?.kind !== 'main-agent')
|
|
198
|
-
return { text: 'Only the current coordinator may manage the shared room task board.' };
|
|
199
|
-
/** The self-correcting roster suffix: the legal member names, for member-not-found. */
|
|
200
|
-
const rosterHint = async () => {
|
|
201
|
-
const state = await backend.getState({ sessionId });
|
|
202
|
-
if (!state.ok)
|
|
203
|
-
return '';
|
|
204
|
-
const names = state.value.members.map(member => member.name);
|
|
205
|
-
return names.length === 0 ? '' : ` The room roster is: ${names.join(', ')}. Retry with one of these names.`;
|
|
206
|
-
};
|
|
207
|
-
/** The self-correcting board suffix: the open tasks (id — title), for task-not-found. */
|
|
208
|
-
const openTasksHint = async () => {
|
|
209
|
-
const state = await backend.getState({ sessionId });
|
|
210
|
-
if (!state.ok)
|
|
211
|
-
return '';
|
|
212
|
-
const open = state.value.tasks.filter(task => task.status === 'pending' || task.status === 'in_progress');
|
|
213
|
-
if (open.length === 0)
|
|
214
|
-
return ' The board has no open tasks right now.';
|
|
215
|
-
return ` Open tasks: ${open.map(task => `${task.id} — "${task.title}" (${task.member}, ${task.status})`).join('; ')}. Retry with one of these ids.`;
|
|
216
|
-
};
|
|
217
|
-
const failureText = async (code) => {
|
|
218
|
-
switch (code) {
|
|
219
|
-
case 'member-not-found':
|
|
220
|
-
return `Unknown member.${await rosterHint()}`;
|
|
221
|
-
case 'empty-text':
|
|
222
|
-
return 'The title must be non-blank; retry with a real title.';
|
|
223
|
-
case 'task-not-found':
|
|
224
|
-
return `No task with that id.${await openTasksHint()}`;
|
|
225
|
-
case 'task-closed':
|
|
226
|
-
return 'That task is already closed (done/cancelled); closed tasks take no further changes.';
|
|
227
|
-
case 'nothing-to-update':
|
|
228
|
-
return 'update needs at least one field to change: a new title and/or blockedBy (null clears the wait).';
|
|
229
|
-
default:
|
|
230
|
-
return `The room rejected the write (${code}).`;
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
switch (args.action) {
|
|
234
|
-
case 'add': {
|
|
235
|
-
if (args.title === undefined || args.member === undefined) {
|
|
236
|
-
return { text: 'room_task add requires both title and member (the roster name owning the task; read room_read for current names).' };
|
|
237
|
-
}
|
|
238
|
-
if (args.blockedBy === null) {
|
|
239
|
-
return { text: 'add takes no null blockedBy — simply omit blockedBy for an unblocked task.' };
|
|
240
|
-
}
|
|
241
|
-
const result = await backend.addTask({
|
|
242
|
-
sessionId,
|
|
243
|
-
member: args.member,
|
|
244
|
-
title: args.title,
|
|
245
|
-
...args.blockedBy === undefined ? {} : { blockedBy: args.blockedBy },
|
|
246
|
-
});
|
|
247
|
-
if (!result.ok)
|
|
248
|
-
return { text: `Could not add the task: ${result.error.code}. ${await failureText(result.error.code)}` };
|
|
249
|
-
return {
|
|
250
|
-
text: `Task "${args.title}" is now on ${args.member}'s lane of the shared room board `
|
|
251
|
-
+ `(id: ${result.value.id}) — visible to every member and the human. `
|
|
252
|
-
+ 'Keep the id to update or close the task later.',
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
case 'close': {
|
|
256
|
-
if (args.taskId === undefined) {
|
|
257
|
-
return { text: `room_task close requires taskId.${await openTasksHint()}` };
|
|
258
|
-
}
|
|
259
|
-
const result = await backend.closeTask({ sessionId, taskId: args.taskId });
|
|
260
|
-
if (!result.ok)
|
|
261
|
-
return { text: `Could not close the task: ${result.error.code}. ${await failureText(result.error.code)}` };
|
|
262
|
-
return { text: `Task ${result.value.id} is done — the shared board shows it closed.` };
|
|
263
|
-
}
|
|
264
|
-
case 'update': {
|
|
265
|
-
if (args.taskId === undefined) {
|
|
266
|
-
return { text: `room_task update requires taskId.${await openTasksHint()}` };
|
|
267
|
-
}
|
|
268
|
-
const result = await backend.updateTask({
|
|
269
|
-
sessionId,
|
|
270
|
-
taskId: args.taskId,
|
|
271
|
-
...args.title === undefined ? {} : { title: args.title },
|
|
272
|
-
...args.blockedBy === undefined ? {} : { blockedBy: args.blockedBy },
|
|
273
|
-
});
|
|
274
|
-
if (!result.ok)
|
|
275
|
-
return { text: `Could not update the task: ${result.error.code}. ${await failureText(result.error.code)}` };
|
|
276
|
-
return { text: `Task ${result.value.id} updated on the shared room board.` };
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
},
|
|
280
|
-
});
|
|
122
|
+
/** The invite receipt's mode line, so the caller can tell the human which mode the member runs. */
|
|
123
|
+
function modeNote(preset) {
|
|
124
|
+
return preset === undefined ? ' Mode: none (its own CLI tools only).' : ` Mode: ${preset}.`;
|
|
281
125
|
}
|
|
282
126
|
/**
|
|
283
127
|
* Build the room_message tool definition bound to the room service: the main
|
|
@@ -290,14 +134,16 @@ export function roomTaskTool(backend) {
|
|
|
290
134
|
export function roomMessageTool(backend) {
|
|
291
135
|
return defineTool({
|
|
292
136
|
name: 'room_message',
|
|
293
|
-
description: '
|
|
137
|
+
description: 'Send a message to another member of the current room (works in any session — it promotes the '
|
|
294
138
|
+ 'session into a room if it is not one yet). '
|
|
295
|
-
+ 'A member is an independent
|
|
296
|
-
+ '
|
|
297
|
-
+ '
|
|
298
|
-
+ '
|
|
299
|
-
+ '
|
|
300
|
-
+ '
|
|
139
|
+
+ 'A member is an independent session: the message runs asynchronously in its own session. '
|
|
140
|
+
+ 'kind "task" (default) hands it work — its final reply is reported back to you automatically, '
|
|
141
|
+
+ 'quoting the message id, so do not poll; kind "notify" is an FYI that is never reported back. '
|
|
142
|
+
+ 'After sending, end your turn: never sleep or poll for the result (room_read included) — the '
|
|
143
|
+
+ 'report arrives on its own. Writing "@name" in a reply reaches nobody; only this tool does. '
|
|
144
|
+
+ 'Pass inReplyTo with a message id (e.g. m12) when answering one. The member argument is a '
|
|
145
|
+
+ 'roster name (read room_read for current names); an unknown name is rejected with the live '
|
|
146
|
+
+ 'roster, so retry with one of those.',
|
|
301
147
|
parameters: {
|
|
302
148
|
member: {
|
|
303
149
|
type: 'string',
|
|
@@ -307,7 +153,24 @@ export function roomMessageTool(backend) {
|
|
|
307
153
|
text: {
|
|
308
154
|
type: 'string',
|
|
309
155
|
required: true,
|
|
310
|
-
description: 'The message text the member receives
|
|
156
|
+
description: 'The message text the member receives.',
|
|
157
|
+
},
|
|
158
|
+
kind: {
|
|
159
|
+
type: 'string',
|
|
160
|
+
enum: ['task', 'notify'],
|
|
161
|
+
description: 'task (default): its reply is reported back to you. notify: FYI, no report.',
|
|
162
|
+
},
|
|
163
|
+
inReplyTo: {
|
|
164
|
+
type: 'string',
|
|
165
|
+
description: 'Optional id of the message this answers (e.g. m12).',
|
|
166
|
+
},
|
|
167
|
+
task: {
|
|
168
|
+
type: 'string',
|
|
169
|
+
description: 'Optional: which task this belongs to — "new" or a task number from room_read (e.g. "2"). Use it when the human\'s latest message was filed under the wrong task: it re-files that message too. Omit to stay in the current task.',
|
|
170
|
+
},
|
|
171
|
+
note: {
|
|
172
|
+
type: 'string',
|
|
173
|
+
description: 'Optional: what you will wait for before acting on this result, in your own words (e.g. "wait until all three greeted, then tell the user once" / "hand it to claude for review as soon as it is done"). The room uses it to decide when to hand you the report.',
|
|
311
174
|
},
|
|
312
175
|
},
|
|
313
176
|
output: {
|
|
@@ -329,7 +192,11 @@ export function roomMessageTool(backend) {
|
|
|
329
192
|
return { text: 'room_message requires a calling agent (exec.agent was undefined).' };
|
|
330
193
|
// No room gate: messageMember PROMOTES the calling session into a room.
|
|
331
194
|
const sessionId = agent.session.id;
|
|
332
|
-
const result = await backend.messageMember({ sessionId, member: args.member, text: args.text
|
|
195
|
+
const result = await backend.messageMember({ sessionId, member: args.member, text: args.text,
|
|
196
|
+
...args.kind === undefined ? {} : { kind: args.kind },
|
|
197
|
+
...args.inReplyTo === undefined ? {} : { inReplyTo: args.inReplyTo },
|
|
198
|
+
...args.note === undefined ? {} : { note: args.note },
|
|
199
|
+
...args.task === undefined ? {} : { task: args.task } });
|
|
333
200
|
if (!result.ok) {
|
|
334
201
|
if (result.error.code === 'member-not-found') {
|
|
335
202
|
const state = await backend.getState({ sessionId });
|
|
@@ -337,14 +204,29 @@ export function roomMessageTool(backend) {
|
|
|
337
204
|
const hint = names.length === 0 ? '' : ` The room roster is: ${names.join(', ')}. Retry with one of these names.`;
|
|
338
205
|
return { text: `Could not message "${args.member}": unknown member.${hint}` };
|
|
339
206
|
}
|
|
207
|
+
if (result.error.code === 'self-message')
|
|
208
|
+
return { text: 'You cannot message yourself; pick another member.' };
|
|
209
|
+
if (result.error.code === 'unknown-task')
|
|
210
|
+
return { text: `No task "${args.task}" in this room; pass "new" or a task number from room_read.` };
|
|
340
211
|
if (result.error.code === 'empty-text') {
|
|
341
212
|
return { text: 'The message text must be non-blank; retry with real content.' };
|
|
342
213
|
}
|
|
343
214
|
return { text: `Could not message "${args.member}": ${result.error.code}.` };
|
|
344
215
|
}
|
|
216
|
+
const value = result.value;
|
|
217
|
+
if (value.held !== undefined) {
|
|
218
|
+
return { text: value.held === 'paused'
|
|
219
|
+
? `Held: the room is paused; the message to ${value.member} is delivered when the human resumes it.`
|
|
220
|
+
: value.held === 'hop-limit'
|
|
221
|
+
? `Held: this chain of messages reached the room's hop limit; the human decides whether ${value.member} gets it.`
|
|
222
|
+
: `Held: the room asks the human to confirm member messages; ${value.member} gets it once confirmed.` };
|
|
223
|
+
}
|
|
345
224
|
return {
|
|
346
|
-
text:
|
|
347
|
-
|
|
225
|
+
text: args.kind === 'notify'
|
|
226
|
+
? `Sent ${value.messageId} to ${value.member} as an FYI; no report will come back.`
|
|
227
|
+
: `Sent ${value.messageId} to ${value.member}. It runs asynchronously; its final reply comes back to you `
|
|
228
|
+
+ 'automatically as a report citing this id. Do not sleep, wait, or poll room_read for it — end your turn; '
|
|
229
|
+
+ 'the human already sees the member\'s reply in the room, so do not repeat it when the report arrives.',
|
|
348
230
|
};
|
|
349
231
|
},
|
|
350
232
|
});
|
|
@@ -353,7 +235,7 @@ export function roomMessageTool(backend) {
|
|
|
353
235
|
export function roomReadTool(backend) {
|
|
354
236
|
return defineTool({
|
|
355
237
|
name: 'room_read',
|
|
356
|
-
description: 'Read the current room roster, coordinator, deliveries, recent outcomes and available harness providers. Reading does not wake any member.',
|
|
238
|
+
description: 'Read the current room: roster with each member\'s role, the coordinator, the tasks with their numbers (for the task parameter), deliveries, recent outcomes and available harness providers. In a session that is not a room yet it returns the invitable providers and modes instead. Reading does not wake any member. Use it to look around, not to wait for a result — reports arrive on their own.',
|
|
357
239
|
parameters: {},
|
|
358
240
|
output: { schema: { type: 'object', additionalProperties: false, properties: { text: { type: 'string', required: true } } },
|
|
359
241
|
render: (_args, value) => [{ type: 'text', text: value.text }] },
|
|
@@ -391,3 +273,54 @@ export function roomPlanTool(backend) {
|
|
|
391
273
|
},
|
|
392
274
|
});
|
|
393
275
|
}
|
|
276
|
+
/**
|
|
277
|
+
* Build the room_help tool definition bound to the room service.
|
|
278
|
+
* @param backend - the room service.
|
|
279
|
+
* @returns a registry-ready tool definition.
|
|
280
|
+
*/
|
|
281
|
+
export function roomHelpTool(backend) {
|
|
282
|
+
return defineTool({
|
|
283
|
+
name: 'room_help',
|
|
284
|
+
description: 'Ask another session — outside this room — for help: typically the session that built the thing you are stuck on, '
|
|
285
|
+
+ 'which still has the whole context. The helper keeps working in its own session (if it is busy, your request waits '
|
|
286
|
+
+ 'in its queue) and its final answer plus a short handoff comes back to you on its own, so end your turn after asking; '
|
|
287
|
+
+ 'never poll. Name the session by its title, its id, or the @ mention the human wrote. A session that is another room '
|
|
288
|
+
+ 'hands the request to its coordinator, or to the member you name (a mention labelled 「成员(房间)」 names one). '
|
|
289
|
+
+ 'The first ask of a session the '
|
|
290
|
+
+ 'human did not @ in this room waits for the human\'s approval. To answer the helper\'s question or ask it more, call '
|
|
291
|
+
+ 'again with the help id and no session.',
|
|
292
|
+
parameters: {
|
|
293
|
+
session: {
|
|
294
|
+
type: 'string',
|
|
295
|
+
description: 'The session to ask (title, id, or @ mention). Omit when continuing a thread with help.',
|
|
296
|
+
},
|
|
297
|
+
member: {
|
|
298
|
+
type: 'string',
|
|
299
|
+
description: 'When the session is another room: the member of that room to ask. Omit to let its coordinator decide.',
|
|
300
|
+
},
|
|
301
|
+
help: {
|
|
302
|
+
type: 'string',
|
|
303
|
+
description: 'The help id of an existing thread, to answer its question or follow up.',
|
|
304
|
+
},
|
|
305
|
+
text: {
|
|
306
|
+
type: 'string',
|
|
307
|
+
required: true,
|
|
308
|
+
description: 'What you need, self-contained: the helper does not see this room. Include the error, what you tried, where things are.',
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
output: {
|
|
312
|
+
schema: { type: 'object', additionalProperties: false, properties: { text: { type: 'string', required: true } } },
|
|
313
|
+
render: (_args, value) => [{ type: 'text', text: value.text }],
|
|
314
|
+
},
|
|
315
|
+
isConcurrencySafe: () => true,
|
|
316
|
+
async execute(args, exec) {
|
|
317
|
+
const agent = exec.agent;
|
|
318
|
+
if (agent === undefined)
|
|
319
|
+
return { text: 'room_help requires a calling agent (exec.agent was undefined).' };
|
|
320
|
+
return { text: await backend.askHelp({ sessionId: agent.session.id, text: args.text,
|
|
321
|
+
...args.session === undefined ? {} : { session: args.session },
|
|
322
|
+
...args.member === undefined ? {} : { member: args.member },
|
|
323
|
+
...args.help === undefined ? {} : { help: args.help } }, agent) };
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
}
|
package/lib/types/tool.d.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* The session-granted room model tools — this package's `./tool` composition
|
|
3
3
|
* entry (the canvas `./agent` pattern) for agent-preset compositions. The row
|
|
4
4
|
* provides NO service (the preset-mount isolate-realm rule forbids service
|
|
5
|
-
* rows), it only registers the model-facing `room_invite` / `
|
|
6
|
-
* `
|
|
5
|
+
* rows), it only registers the model-facing `room_invite` / `room_message` /
|
|
6
|
+
* `room_read` / `room_plan` tools into the host tools registry, delegating to the global
|
|
7
7
|
* `ctx.room` service the main row provides at the profile root — the official
|
|
8
8
|
* tool-row shape (the shipped `tool-bash` rows work the same way). Granting
|
|
9
9
|
* is therefore per-session: a preset names the row, its sessions get the
|
package/lib/types/tool.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { roomInviteTool, roomMessageTool,
|
|
1
|
+
import { roomHelpTool, roomInviteTool, roomMessageTool, roomReadTool, roomPlanTool } from './tool-definition.js';
|
|
2
2
|
const PACKAGE_NAME = '@khorsheed/dsh-room';
|
|
3
3
|
/**
|
|
4
4
|
* Tag the model-visible tools with their origin (AGENTS.md § Tool origin
|
|
@@ -49,7 +49,8 @@ export function apply(ctx) {
|
|
|
49
49
|
if (typeof service.readRoomContext === 'function')
|
|
50
50
|
toolsCtx.effect(() => toolsCtx.tools.register(definePluginTool(roomReadTool(service))), 'room-tool: room_read tool');
|
|
51
51
|
toolsCtx.effect(() => toolsCtx.tools.register(definePluginTool(roomInviteTool(service))), 'room-tool: room_invite tool');
|
|
52
|
-
toolsCtx.effect(() => toolsCtx.tools.register(definePluginTool(roomTaskTool(service))), 'room-tool: room_task tool');
|
|
53
52
|
toolsCtx.effect(() => toolsCtx.tools.register(definePluginTool(roomMessageTool(service))), 'room-tool: room_message tool');
|
|
53
|
+
if (typeof service.askHelp === 'function')
|
|
54
|
+
toolsCtx.effect(() => toolsCtx.tools.register(definePluginTool(roomHelpTool(service))), 'room-tool: room_help tool');
|
|
54
55
|
});
|
|
55
56
|
}
|