@khorsheed/dsh-room 0.2.2 → 0.2.3
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 +53 -0
- package/README.en.md +29 -62
- package/README.i18n.yaml +2 -2
- package/README.md +28 -61
- package/lib/client.js +13097 -4182
- package/lib/index.js +4045 -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 +92 -4
- package/lib/types/dispatch.js +477 -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 +1415 -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
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Member messaging: one path for every agent-originated message in a room —
|
|
3
|
+
* the coordinator's delegations, free member-to-member messages (`task`
|
|
4
|
+
* gets the recipient's reply reported back, `notify` is FYI), and delivery
|
|
5
|
+
* reports. Each message is either journaled as a `room/dispatch` and handed
|
|
6
|
+
* to the engine, or HELD as a pending relay carrying its would-be dispatch,
|
|
7
|
+
* because the room is paused, the causal chain reached the hop limit, or the
|
|
8
|
+
* room asks the human to confirm member messages. Releasing a held message
|
|
9
|
+
* creates exactly the dispatch it would have been.
|
|
10
|
+
*
|
|
11
|
+
* Every dispatch knows its `hop` (position in the chain that a human message
|
|
12
|
+
* starts) and renders an envelope line on delivery — who sent it, its
|
|
13
|
+
* `#m<seq>` id, and whether a reply is expected — so a member can tell
|
|
14
|
+
* concurrent requests apart and the report names the message it answers.
|
|
15
|
+
* @module @khorsheed/dsh-room/messaging
|
|
16
|
+
*/
|
|
17
|
+
import { randomUUID } from 'node:crypto';
|
|
18
|
+
import { coordinatorMember, memberId, replay } from './journal.js';
|
|
19
|
+
/** `RoomHeldDispatch.task` marker: this send starts a task rooted at the dispatch itself. */
|
|
20
|
+
export const SELF_TASK = '@self';
|
|
21
|
+
/** The id members and the UI use for a dispatch. */
|
|
22
|
+
export function messageId(seq) {
|
|
23
|
+
return `m${seq}`;
|
|
24
|
+
}
|
|
25
|
+
/** Parse `m12` / `#m12` / `12` back to a dispatch seq. */
|
|
26
|
+
export function parseMessageRef(ref) {
|
|
27
|
+
const match = ref === undefined ? null : /^#?m?(\d+)$/.exec(ref.trim());
|
|
28
|
+
return match === null ? undefined : Number(match[1]);
|
|
29
|
+
}
|
|
30
|
+
/** The hop a dispatch sits at (legacy dispatches count as a chain start). */
|
|
31
|
+
export function dispatchHop(events, seq) {
|
|
32
|
+
const event = events.find(row => row.seq === seq);
|
|
33
|
+
return event?.type === 'room/dispatch' ? event.data.hop ?? 1 : 1;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The hop of whatever a member is working on right now: its running
|
|
37
|
+
* delivery's hop, or 0 when it is not inside a room delivery (a human
|
|
38
|
+
* chatting with the room's agent directly) so its sends start a chain.
|
|
39
|
+
*/
|
|
40
|
+
export function workingHop(events, state, member) {
|
|
41
|
+
const run = state.runs.find(entry => entry.member === member.name && entry.state === 'running');
|
|
42
|
+
const seq = run?.runId === undefined ? undefined : Number(run.runId.split(':', 1)[0]);
|
|
43
|
+
return seq === undefined || !Number.isInteger(seq) ? 0 : dispatchHop(events, seq);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Why a message must wait for the human, if it must. A paused room holds
|
|
47
|
+
* everything agents send; a chain past the limit stops; `confirm` holds
|
|
48
|
+
* member-to-member messages only (never the coordinator's delegations or
|
|
49
|
+
* reports, which the human already asked for).
|
|
50
|
+
*/
|
|
51
|
+
export function holdReason(state, hop, origin) {
|
|
52
|
+
const settings = state.settings;
|
|
53
|
+
if (settings === undefined)
|
|
54
|
+
return undefined;
|
|
55
|
+
if (settings.paused)
|
|
56
|
+
return 'paused';
|
|
57
|
+
if (hop > settings.hopLimit)
|
|
58
|
+
return 'hop-limit';
|
|
59
|
+
if (origin === 'member' && settings.memberMessages === 'confirm')
|
|
60
|
+
return 'confirm';
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Journal one agent message: a dispatch or a held relay with its intent. The caller
|
|
65
|
+
* flushes and, on delivery, hands `seq` to the engine.
|
|
66
|
+
* @param session - the room session.
|
|
67
|
+
* @param message - recipient, text and the would-be dispatch.
|
|
68
|
+
* @param skipHold - release path: deliver regardless of the hold rules.
|
|
69
|
+
* @returns the dispatch seq, or the hold.
|
|
70
|
+
*/
|
|
71
|
+
export function sendOrHold(session, message, skipHold = false) {
|
|
72
|
+
const events = session.snapshotEvents();
|
|
73
|
+
const state = replay(events);
|
|
74
|
+
const reason = skipHold ? undefined : holdReason(state, message.intent.hop, message.intent.origin);
|
|
75
|
+
if (reason !== undefined) {
|
|
76
|
+
const relayId = randomUUID();
|
|
77
|
+
session.append('room/relay', {
|
|
78
|
+
id: relayId, from: message.fromName, to: message.to.name, content: message.text, reason, intent: message.intent,
|
|
79
|
+
});
|
|
80
|
+
return { delivered: false, reason, relayId };
|
|
81
|
+
}
|
|
82
|
+
const targetId = memberId(events, message.to);
|
|
83
|
+
const intent = message.intent;
|
|
84
|
+
const id = randomUUID();
|
|
85
|
+
// A send that starts a task of its own (no human message behind it) roots the task at itself.
|
|
86
|
+
const task = intent.task === SELF_TASK ? id : intent.task;
|
|
87
|
+
const dispatch = session.append('room/dispatch', {
|
|
88
|
+
id, origin: intent.origin,
|
|
89
|
+
...task === undefined ? {} : { task },
|
|
90
|
+
...intent.parent === undefined ? {} : { parent: intent.parent }, targets: [message.to.name], targetIds: [targetId], text: message.text, hop: intent.hop,
|
|
91
|
+
...intent.from === undefined ? {} : { from: intent.from },
|
|
92
|
+
...intent.kind === undefined ? {} : { kind: intent.kind },
|
|
93
|
+
...intent.inReplyTo === undefined ? {} : { inReplyTo: intent.inReplyTo },
|
|
94
|
+
...intent.note === undefined ? {} : { note: intent.note },
|
|
95
|
+
...intent.replyTo === undefined ? {} : { replyTo: intent.replyTo },
|
|
96
|
+
...intent.reportFor === undefined ? {} : { reportFor: intent.reportFor },
|
|
97
|
+
...intent.plan === undefined ? {} : { plan: intent.plan },
|
|
98
|
+
...intent.help === undefined ? {} : { help: intent.help },
|
|
99
|
+
});
|
|
100
|
+
return { delivered: true, seq: dispatch.seq };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The would-be dispatch of a held relay, re-anchored for release: a chain
|
|
104
|
+
* the human lets continue past the hop limit starts a fresh budget.
|
|
105
|
+
*/
|
|
106
|
+
export function releaseIntent(relay) {
|
|
107
|
+
if (relay.intent === undefined)
|
|
108
|
+
return undefined;
|
|
109
|
+
return relay.reason === 'hop-limit' ? { ...relay.intent, hop: 1 } : relay.intent;
|
|
110
|
+
}
|
|
111
|
+
const OUTCOME_LABEL = {
|
|
112
|
+
done: '完成', cancelled: '已取消', failed: '失败', uncertain: '结果不确定',
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* The envelope line a delivered message opens with, or undefined for a
|
|
116
|
+
* dispatch without one (legacy journals). Names resolve against the CURRENT
|
|
117
|
+
* roster, falling back to the last known name of a member who left.
|
|
118
|
+
* @param events - the room log.
|
|
119
|
+
* @param seq - the dispatch being delivered.
|
|
120
|
+
* @returns the envelope line.
|
|
121
|
+
*/
|
|
122
|
+
export function envelopeLine(events, seq) {
|
|
123
|
+
const event = events.find(row => row.seq === seq);
|
|
124
|
+
if (event?.type !== 'room/dispatch')
|
|
125
|
+
return undefined;
|
|
126
|
+
const dispatch = event.data;
|
|
127
|
+
const state = replay(events);
|
|
128
|
+
const nameOf = (id) => id === undefined ? undefined
|
|
129
|
+
: state.members.find(member => memberId(events, member) === id)?.name
|
|
130
|
+
?? events.reduce((name, row) => row.type === 'room/member-added' && row.data.id === id ? row.data.name : name, undefined);
|
|
131
|
+
const reply = dispatch.inReplyTo === undefined ? '' : ` · 回复 #${messageId(dispatch.inReplyTo)}`;
|
|
132
|
+
switch (dispatch.origin) {
|
|
133
|
+
case 'human':
|
|
134
|
+
case undefined:
|
|
135
|
+
// The human's own words need no envelope: an unmarked message is theirs.
|
|
136
|
+
return undefined;
|
|
137
|
+
case 'report': {
|
|
138
|
+
// Plan bookkeeping reports (goal-start:/reconcile:) carry their own text.
|
|
139
|
+
if (dispatch.reportFor === undefined || !/^\d+:/.test(dispatch.reportFor))
|
|
140
|
+
return undefined;
|
|
141
|
+
const source = Number(dispatch.reportFor.split(':', 1)[0]);
|
|
142
|
+
const sender = nameOf(dispatch.reportFor?.split(':').slice(1).join(':'));
|
|
143
|
+
const outcome = events.filter(row => row.type === 'room/delivery-state' && row.data.id === dispatch.reportFor).at(-1);
|
|
144
|
+
const label = outcome?.type === 'room/delivery-state' ? OUTCOME_LABEL[outcome.data.state] ?? outcome.data.state : '';
|
|
145
|
+
return `【#${messageId(source)} 的回报${sender === undefined ? '' : ` · 来自 ${sender}`}${label === '' ? '' : ` · ${label}`}】(这条回复用户已在房间里看到,不要复述)`;
|
|
146
|
+
}
|
|
147
|
+
case 'relay':
|
|
148
|
+
return undefined;
|
|
149
|
+
case 'help':
|
|
150
|
+
return `【来自协作间「${dispatch.help?.roomTitle ?? ''}」· ${dispatch.help?.from ?? ''} 的求助 · #${messageId(seq)}】(对方在另一个会话里,看不到这个房间;你这一轮的最终回复会自动送回对方,写成给对方看的答复,不用另外回复)`;
|
|
151
|
+
default: {
|
|
152
|
+
const sender = nameOf(dispatch.from)
|
|
153
|
+
?? (dispatch.origin === 'coordinator' ? coordinatorMember(state, events)?.name : undefined) ?? '成员';
|
|
154
|
+
if (dispatch.kind === 'notify')
|
|
155
|
+
return `【来自 ${sender} · #${messageId(seq)} · 知会${reply}】`;
|
|
156
|
+
const back = dispatch.replyTo === undefined && dispatch.plan === undefined ? '' : '(你这一轮的最终回复会自动回报给对方,不用另外回复)';
|
|
157
|
+
return `【来自 ${sender} · #${messageId(seq)} · 派活${reply}】${back}`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { coordinatorMember, memberId, replay } from './journal.js';
|
|
3
3
|
import { activePlanMs, dependencyResults, admitPlanAttempt, changePlan, parsePlanCommand, planCommandSignature, queuePlanAttempt, readyPlanTasks, recoverPlan, settlePlanAttempt } from './plan.js';
|
|
4
|
+
import { runChildSessionId, runChildTurn } from './dispatch.js';
|
|
4
5
|
export function readPlan(room) {
|
|
5
6
|
const event = room.snapshotEvents().filter(event => event.type === 'room/plan-state').at(-1);
|
|
6
7
|
return event?.type === 'room/plan-state' ? structuredClone(event.data) : undefined;
|
|
@@ -192,9 +193,14 @@ export class PlanService {
|
|
|
192
193
|
const deliveryId = `${dispatch.seq}:${target}`;
|
|
193
194
|
const member = state.members.find(member => memberId(events, member) === target);
|
|
194
195
|
const run = state.runs.find(run => run.member === member?.name);
|
|
195
|
-
if (run?.runId === deliveryId && ['running', 'failed'].includes(run.state))
|
|
196
|
+
if (run?.runId === deliveryId && ['running', 'failed'].includes(run.state)) {
|
|
197
|
+
const childSessionId = runChildSessionId(room.id, member);
|
|
198
|
+
const childTurn = runChildTurn(childSessionId === undefined ? undefined : this.ctx.sessions.get(childSessionId));
|
|
196
199
|
room.append('room/run-state', { member: run.member, runId: run.runId, startedAt: run.startedAt, state: attempt.status === 'submitted' ? 'done' : 'failed',
|
|
200
|
+
...childSessionId === undefined ? {} : { childSessionId },
|
|
201
|
+
...childTurn === undefined ? {} : { childTurn },
|
|
197
202
|
...attempt.status === 'failed' ? { error: attempt.submission?.summary ?? 'Human reconciled the attempt as failed' } : {} });
|
|
203
|
+
}
|
|
198
204
|
room.append('room/delivery-state', { id: deliveryId, dispatchSeq: dispatch.seq, memberId: target,
|
|
199
205
|
state: attempt.status === 'submitted' ? 'done' : 'failed', text: attempt.submission?.summary ?? 'Human reconciliation' });
|
|
200
206
|
for (const oldReport of events) {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The report gate: decides only WHEN a delivery's report is handed to the
|
|
3
|
+
* member waiting on it — now (a round that wakes it), or held in its inbox
|
|
4
|
+
* until more results arrive. What the recipient then says or does next is
|
|
5
|
+
* its own call; the gate never edits, drops or summarizes a report, and a
|
|
6
|
+
* held report still reaches the recipient (a native agent's inbox, or carried
|
|
7
|
+
* by a CLI member's next round).
|
|
8
|
+
*
|
|
9
|
+
* Rules answer first: failures and uncertain outcomes, the last outstanding
|
|
10
|
+
* result, the `each` setting, and an unavailable decision model all hand the
|
|
11
|
+
* report over now. Otherwise the decision model answers the `report-timing`
|
|
12
|
+
* scenario question (decisions.ts) from what the recipient said it waits for,
|
|
13
|
+
* how long the human has been waiting, and the progress of everything still
|
|
14
|
+
* out; the room's threshold turns its probability into now / later.
|
|
15
|
+
* @module @khorsheed/dsh-room/report-gate
|
|
16
|
+
*/
|
|
17
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
18
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session/types';
|
|
19
|
+
import type { RoomDecisionCall, RoomDelivery, RoomMember } from './types.ts';
|
|
20
|
+
/** The gate's verdict. */
|
|
21
|
+
export interface GateVerdict {
|
|
22
|
+
readonly hold: boolean;
|
|
23
|
+
readonly reason: string;
|
|
24
|
+
readonly probability?: number;
|
|
25
|
+
/** The decision-model call behind it, when one went out. */
|
|
26
|
+
readonly call?: RoomDecisionCall;
|
|
27
|
+
}
|
|
28
|
+
/** One arrived report, as the gate sees it. */
|
|
29
|
+
export interface ArrivedReport {
|
|
30
|
+
readonly from: string;
|
|
31
|
+
readonly outcome: string;
|
|
32
|
+
readonly text: string;
|
|
33
|
+
}
|
|
34
|
+
/** Deliveries still out that report to this member (queued or running), newest first. */
|
|
35
|
+
export declare function outstandingFor(deliveries: readonly RoomDelivery[], recipientId: string, except: string): readonly RoomDelivery[];
|
|
36
|
+
/**
|
|
37
|
+
* The state the decision model judges.
|
|
38
|
+
* @param events - the room log.
|
|
39
|
+
* @param recipient - the member waiting on the report.
|
|
40
|
+
* @param arrived - this report plus the ones held for the same member.
|
|
41
|
+
* @param now - the clock.
|
|
42
|
+
* @returns a JSON-safe state.
|
|
43
|
+
*/
|
|
44
|
+
export declare function gateState(events: readonly SessionEvent[], recipient: RoomMember, arrived: readonly ArrivedReport[], now: number): unknown;
|
|
45
|
+
/**
|
|
46
|
+
* Decide whether to hand a report over now.
|
|
47
|
+
* @param ctx - host context (probes `typesafe`).
|
|
48
|
+
* @param input - what the rules and the model read.
|
|
49
|
+
* @returns the verdict with its reason.
|
|
50
|
+
*/
|
|
51
|
+
export declare function judgeReport(ctx: Context, input: {
|
|
52
|
+
readonly timing: 'judge' | 'each';
|
|
53
|
+
readonly outcome: string;
|
|
54
|
+
readonly outstanding: number;
|
|
55
|
+
/** The "hand it over now" probability at or above which the report is delivered (default 0.5). */
|
|
56
|
+
readonly threshold?: number;
|
|
57
|
+
readonly state: () => unknown;
|
|
58
|
+
}): Promise<GateVerdict>;
|
|
59
|
+
/** One open task offered to the attribution question. */
|
|
60
|
+
export interface OpenTaskSummary {
|
|
61
|
+
readonly number: number;
|
|
62
|
+
readonly title: string;
|
|
63
|
+
readonly running: readonly string[];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Suggest which open task a new human message continues, or `new`. Only
|
|
67
|
+
* asked while some task is still open; the coordinator sees the suggestion
|
|
68
|
+
* and can override it when it dispatches (`task`).
|
|
69
|
+
* @param ctx - host context (probes `typesafe`).
|
|
70
|
+
* @param message - the new human message text.
|
|
71
|
+
* @param open - the open tasks.
|
|
72
|
+
* @returns the suggested task number (or `new`) with its probability and the call, or `new` by the `unavailable` rule.
|
|
73
|
+
*/
|
|
74
|
+
export declare function suggestTask(ctx: Context, message: string, open: readonly OpenTaskSummary[]): Promise<{
|
|
75
|
+
readonly task: number | 'new';
|
|
76
|
+
/** The rule that decided (`unavailable` after a failed or missing model); absent = the model decided. */
|
|
77
|
+
readonly rule?: string;
|
|
78
|
+
readonly probability?: number;
|
|
79
|
+
readonly call?: RoomDecisionCall;
|
|
80
|
+
}>;
|
|
81
|
+
//# sourceMappingURL=report-gate.d.ts.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { memberId, replay } from './journal.js';
|
|
2
|
+
import { askDecisionModel, DECISION_SCENARIOS, hasDecisionModel } from './decisions.js';
|
|
3
|
+
/** Deliveries still out that report to this member (queued or running), newest first. */
|
|
4
|
+
export function outstandingFor(deliveries, recipientId, except) {
|
|
5
|
+
return deliveries.filter(row => row.id !== except && row.replyTo === recipientId && (row.status === 'queued' || row.status === 'running'));
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The state the decision model judges.
|
|
9
|
+
* @param events - the room log.
|
|
10
|
+
* @param recipient - the member waiting on the report.
|
|
11
|
+
* @param arrived - this report plus the ones held for the same member.
|
|
12
|
+
* @param now - the clock.
|
|
13
|
+
* @returns a JSON-safe state.
|
|
14
|
+
*/
|
|
15
|
+
export function gateState(events, recipient, arrived, now) {
|
|
16
|
+
const state = replay(events);
|
|
17
|
+
const recipientId = memberId(events, recipient);
|
|
18
|
+
const lastHuman = events.filter(event => event.type === 'user/message' && event.data.source.kind === 'user').at(-1);
|
|
19
|
+
const lastSpoken = events.filter(event => event.type === 'assistant/message' && event.seq > (lastHuman?.seq ?? -1)).at(-1);
|
|
20
|
+
const nameOf = (id) => state.members.find(member => memberId(events, member) === id)?.name ?? id;
|
|
21
|
+
const since = lastHuman?.seq ?? -1;
|
|
22
|
+
const batch = (state.deliveries ?? []).filter(row => row.replyTo === recipientId && row.dispatchSeq > since);
|
|
23
|
+
const running = (state.executions ?? []).filter(row => row.state === 'running');
|
|
24
|
+
const notes = batch.flatMap((row) => {
|
|
25
|
+
const event = events.find(entry => entry.seq === row.dispatchSeq);
|
|
26
|
+
return event?.type === 'room/dispatch' && event.data.note !== undefined ? [event.data.note] : [];
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
userRequest: lastHuman?.type === 'user/message'
|
|
30
|
+
? lastHuman.data.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('\n').slice(0, 2000) : '',
|
|
31
|
+
coordinatorNotes: [...new Set(notes)],
|
|
32
|
+
userWaitingSeconds: Math.max(0, Math.round((now - (lastSpoken?.time ?? lastHuman?.time ?? now)) / 1000)),
|
|
33
|
+
progress: {
|
|
34
|
+
done: batch.filter(row => !['queued', 'running'].includes(row.status)).length,
|
|
35
|
+
total: batch.length,
|
|
36
|
+
running: batch.filter(row => row.status === 'queued' || row.status === 'running').map((row) => {
|
|
37
|
+
const execution = running.find(entry => entry.id === row.id);
|
|
38
|
+
return { member: nameOf(row.memberId), task: row.text.slice(0, 200), seconds: execution === undefined ? 0 : Math.round((now - execution.startedAt) / 1000) };
|
|
39
|
+
}),
|
|
40
|
+
},
|
|
41
|
+
arrived: arrived.map(report => ({ ...report, text: report.text.slice(0, 1500) })),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Decide whether to hand a report over now.
|
|
46
|
+
* @param ctx - host context (probes `typesafe`).
|
|
47
|
+
* @param input - what the rules and the model read.
|
|
48
|
+
* @returns the verdict with its reason.
|
|
49
|
+
*/
|
|
50
|
+
export async function judgeReport(ctx, input) {
|
|
51
|
+
if (input.timing === 'each')
|
|
52
|
+
return { hold: false, reason: 'setting' };
|
|
53
|
+
if (input.outcome !== 'done')
|
|
54
|
+
return { hold: false, reason: 'failure' };
|
|
55
|
+
if (input.outstanding === 0)
|
|
56
|
+
return { hold: false, reason: 'last' };
|
|
57
|
+
if (!hasDecisionModel(ctx))
|
|
58
|
+
return { hold: false, reason: 'unavailable' };
|
|
59
|
+
const asked = await askDecisionModel(ctx, DECISION_SCENARIOS['report-timing'].question, input.state());
|
|
60
|
+
if (!asked.ok || typeof asked.answer !== 'number') {
|
|
61
|
+
return { hold: false, reason: 'unavailable', ...asked.call === undefined ? {} : { call: asked.call } };
|
|
62
|
+
}
|
|
63
|
+
return { hold: asked.answer < (input.threshold ?? 0.5), reason: 'model', probability: asked.answer, call: asked.call };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Suggest which open task a new human message continues, or `new`. Only
|
|
67
|
+
* asked while some task is still open; the coordinator sees the suggestion
|
|
68
|
+
* and can override it when it dispatches (`task`).
|
|
69
|
+
* @param ctx - host context (probes `typesafe`).
|
|
70
|
+
* @param message - the new human message text.
|
|
71
|
+
* @param open - the open tasks.
|
|
72
|
+
* @returns the suggested task number (or `new`) with its probability and the call, or `new` by the `unavailable` rule.
|
|
73
|
+
*/
|
|
74
|
+
export async function suggestTask(ctx, message, open) {
|
|
75
|
+
if (!hasDecisionModel(ctx))
|
|
76
|
+
return { task: 'new', rule: 'unavailable' };
|
|
77
|
+
const scenario = DECISION_SCENARIOS['task-filing'].question;
|
|
78
|
+
const criteria = Object.fromEntries(open.map(job => [String(job.number),
|
|
79
|
+
`Continues task ${job.number}: "${job.title.slice(0, 200)}"${job.running.length === 0 ? '' : ` (still running: ${job.running.join(', ')})`}`]));
|
|
80
|
+
criteria['new'] = 'Starts a new, unrelated task';
|
|
81
|
+
const asked = await askDecisionModel(ctx, { ...scenario, criteria }, {
|
|
82
|
+
message: message.slice(0, 2000),
|
|
83
|
+
openTasks: open.map(job => ({ number: job.number, title: job.title.slice(0, 200), running: job.running })),
|
|
84
|
+
});
|
|
85
|
+
if (!asked.ok || typeof asked.answer !== 'string')
|
|
86
|
+
return { task: 'new', rule: 'unavailable', ...asked.call === undefined ? {} : { call: asked.call } };
|
|
87
|
+
const probability = asked.probabilities?.[asked.answer] ?? 0;
|
|
88
|
+
return { task: asked.answer === 'new' ? 'new' : Number(asked.answer), probability, call: asked.call };
|
|
89
|
+
}
|
|
@@ -1,4 +1,29 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The room's model-facing tool DEFINITIONS, `room_invite` and
|
|
3
|
+
* `room_message` (plus `room_read` and `room_plan`): 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.
|
|
7
|
+
* `room_invite` lands in the same invite path as the human dialog
|
|
8
|
+
* (`invitedBy: 'agent'`), producing an identical member record; `room_message` dispatches
|
|
9
|
+
* a message to a member through the same engine path as the human's
|
|
10
|
+
* `@member text`, minus the user bubble (the caller is the room's own agent).
|
|
11
|
+
*
|
|
12
|
+
* Since the tool-row split (M4') the core never REGISTERS these tools: the
|
|
13
|
+
* factories are consumed by the package's `./tool` subpath row
|
|
14
|
+
* ({@link file://./tool.ts}), which mounts them inside agent-preset
|
|
15
|
+
* compositions (session-granted, never at the profile root). The definitions
|
|
16
|
+
* are therefore returned UNTAGGED — the registering row applies its own
|
|
17
|
+
* tool-origin tag (AGENTS.md § Tool origin tagging — the owner is whichever
|
|
18
|
+
* package mounts the row).
|
|
19
|
+
*
|
|
20
|
+
* Internal module: not on the package's exports map (consumers reach it only
|
|
21
|
+
* through `./src/*`).
|
|
22
|
+
*
|
|
23
|
+
* @module @khorsheed/dsh-room
|
|
24
|
+
*/
|
|
25
|
+
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
26
|
+
import type { RoomGetStateRequest, RoomGetStateResult, RoomInviteRequest, RoomInviteResult, RoomMessageRequest, RoomMessageResult } from './types.ts';
|
|
2
27
|
/** The slice of RoomService the tool drives. */
|
|
3
28
|
export interface RoomInviteToolBackend {
|
|
4
29
|
/** Validate and journal an invitation (see RoomService.inviteMember). */
|
|
@@ -10,25 +35,6 @@ export interface RoomInviteToolBackend {
|
|
|
10
35
|
* @returns a registry-ready tool definition.
|
|
11
36
|
*/
|
|
12
37
|
export declare function roomInviteTool(backend: RoomInviteToolBackend): import("@deepseek-ai/dsh-tools").ToolDefinition;
|
|
13
|
-
/** The slice of RoomService the room_task tool drives. */
|
|
14
|
-
export interface RoomTaskToolBackend {
|
|
15
|
-
/** Open a pending task on a member's lane (see RoomService.addTask). */
|
|
16
|
-
addTask(request: RoomAddTaskRequest): Promise<RoomAddTaskResult>;
|
|
17
|
-
/** Close an open task (see RoomService.closeTask). */
|
|
18
|
-
closeTask(request: RoomCloseTaskRequest): Promise<RoomCloseTaskResult>;
|
|
19
|
-
/** Edit an open task's title/blockedBy (see RoomService.updateTask). */
|
|
20
|
-
updateTask(request: RoomUpdateTaskRequest): Promise<RoomUpdateTaskResult>;
|
|
21
|
-
/** Replay the room state (the error texts read the roster/open tasks from it). */
|
|
22
|
-
getState(request: RoomGetStateRequest): Promise<RoomGetStateResult>;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Build the room_task tool definition bound to the room service. Every write
|
|
26
|
-
* goes through the same host functions as the capsule UI, so a task the agent
|
|
27
|
-
* opens is a public commitment indistinguishable from a human-added one.
|
|
28
|
-
* @param backend - the room service.
|
|
29
|
-
* @returns a registry-ready tool definition.
|
|
30
|
-
*/
|
|
31
|
-
export declare function roomTaskTool(backend: RoomTaskToolBackend): import("@deepseek-ai/dsh-tools").ToolDefinition;
|
|
32
38
|
/** The slice of RoomService the room_message tool drives. */
|
|
33
39
|
export interface RoomMessageToolBackend {
|
|
34
40
|
/** Dispatch one message to one member (see RoomService.messageMember). */
|
|
@@ -53,4 +59,21 @@ export declare function roomReadTool(backend: {
|
|
|
53
59
|
export declare function roomPlanTool(backend: {
|
|
54
60
|
commandPlan(sessionId: string, command: string): Promise<string>;
|
|
55
61
|
}): import("@deepseek-ai/dsh-tools").ToolDefinition;
|
|
62
|
+
/** The slice of RoomService the help tool drives. */
|
|
63
|
+
export interface RoomHelpToolBackend {
|
|
64
|
+
/** Ask another session for help, or write again on a thread (see RoomService.askHelp). */
|
|
65
|
+
askHelp(request: {
|
|
66
|
+
readonly sessionId: string;
|
|
67
|
+
readonly session?: string;
|
|
68
|
+
readonly member?: string;
|
|
69
|
+
readonly help?: string;
|
|
70
|
+
readonly text: string;
|
|
71
|
+
}, caller: Agent): Promise<string>;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Build the room_help tool definition bound to the room service.
|
|
75
|
+
* @param backend - the room service.
|
|
76
|
+
* @returns a registry-ready tool definition.
|
|
77
|
+
*/
|
|
78
|
+
export declare function roomHelpTool(backend: RoomHelpToolBackend): import("@deepseek-ai/dsh-tools").ToolDefinition;
|
|
56
79
|
//# sourceMappingURL=tool-definition.d.ts.map
|