@huynle/hub 0.9.2 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +320 -0
- package/Dockerfile +5 -1
- package/README.md +154 -77
- package/package.json +7 -4
- package/packages/claude-code/package.json +1 -1
- package/packages/cli/dist/hub-cli.js +534 -261
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/cli.ts +36 -1
- package/packages/cli/src/subscriptions.ts +49 -0
- package/packages/codex/package.json +1 -1
- package/packages/codex/src/sessions.ts +10 -6
- package/packages/codex/src/spoke.ts +2 -1
- package/packages/node/package.json +1 -1
- package/packages/preset/package.json +1 -1
- package/packages/protocol/package.json +1 -1
- package/packages/protocol/schema/app-delivery/v1.schema.json +7 -5
- package/packages/protocol/schema/app-front/v1.schema.json +410 -0
- package/packages/protocol/src/app-front.ts +293 -0
- package/packages/protocol/src/concierge.ts +10 -1
- package/packages/protocol/src/context-cards.ts +59 -0
- package/packages/protocol/src/credentials.ts +39 -2
- package/packages/protocol/src/index.ts +81 -11
- package/packages/runtime-openshell/package.json +1 -1
- package/packages/runtime-sdk/package.json +1 -1
- package/packages/sdk/package.json +1 -1
- package/packages/sdk/src/app-front.ts +524 -0
- package/packages/sdk/src/client.ts +37 -4
- package/packages/sdk/src/envelope.ts +9 -2
- package/packages/sdk/src/errors.ts +1 -0
- package/packages/sdk/src/index.ts +1 -0
- package/packages/sdk/src/types.ts +39 -1
- package/packages/spoke-sdk/package.json +1 -1
- package/packages/spoke-sdk/src/delivery.ts +17 -1
- package/packages/spoke-sdk/src/harness-spoke.ts +4 -3
- package/packages/voice-client/README.md +146 -0
- package/packages/voice-client/src/vad/LICENSE +21 -0
- package/src/central/agent/acting-routes.ts +17 -3
- package/src/central/agent/acting.ts +12 -3
- package/src/central/agent/actor.ts +6 -1
- package/src/central/agent/app-decisions.ts +146 -0
- package/src/central/agent/app-tokens.ts +9 -3
- package/src/central/agent/audit.ts +3 -0
- package/src/central/agent/connect-routes.ts +64 -0
- package/src/central/agent/connect.ts +868 -0
- package/src/central/agent/conversation.ts +625 -0
- package/src/central/agent/decisions.ts +71 -6
- package/src/central/agent/delivery.ts +3 -0
- package/src/central/agent/directory.ts +5 -0
- package/src/central/agent/errors.ts +2 -0
- package/src/central/agent/grant-requests.ts +46 -9
- package/src/central/agent/grants.ts +13 -2
- package/src/central/agent/message-limits.ts +2 -2
- package/src/central/agent/message-routes.ts +13 -4
- package/src/central/agent/read-routes.ts +16 -2
- package/src/central/agent/routes.ts +108 -6
- package/src/central/agent/speech-routes.ts +316 -0
- package/src/central/agent/subscription-routes.ts +174 -0
- package/src/central/agent/subscriptions.ts +980 -0
- package/src/central/agent-data.ts +43 -1
- package/src/central/backend/memory-counters.ts +2 -2
- package/src/central/backend/types.ts +5 -2
- package/src/central/backend/valkey-counters.ts +5 -4
- package/src/central/client-address.ts +16 -0
- package/src/central/concierge/board-render.ts +52 -1
- package/src/central/concierge/board-routes.ts +25 -2
- package/src/central/concierge/board.ts +11 -1
- package/src/central/concierge/context.ts +29 -8
- package/src/central/concierge/grant-request.ts +216 -0
- package/src/central/concierge/prompt.ts +49 -2
- package/src/central/concierge/session-routes.ts +3 -0
- package/src/central/config.ts +64 -3
- package/src/central/context-cards/budget.ts +121 -0
- package/src/central/context-cards/changes.ts +29 -0
- package/src/central/context-cards/config.ts +145 -0
- package/src/central/context-cards/engine.ts +379 -0
- package/src/central/context-cards/inputs.ts +111 -0
- package/src/central/context-cards/pickup.ts +134 -0
- package/src/central/context-cards/registry.ts +89 -0
- package/src/central/context-cards/routes.ts +37 -0
- package/src/central/context-cards/service.ts +367 -0
- package/src/central/context-cards/store.ts +67 -0
- package/src/central/context-cards/types.ts +88 -0
- package/src/central/fetcher.ts +10 -2
- package/src/central/front-relays.ts +423 -0
- package/src/central/http.ts +547 -58
- package/src/central/human-activity.ts +453 -0
- package/src/central/llm/chat.ts +133 -0
- package/src/central/registry.ts +162 -9
- package/src/central/speech/service.ts +9 -0
- package/src/central/spoke-ws.ts +13 -0
- package/src/central/store.ts +9 -12
- package/src/config.ts +30 -13
- package/src/index.ts +45 -43
- package/src/rpc.ts +33 -4
- package/src/spoke/concierge-agent.ts +1 -1
- package/src/spoke/conversation.ts +6 -75
- package/src/spoke/deliver.ts +18 -8
- package/src/spoke/hub-rpc.ts +7 -2
- package/src/spoke/hub-tool-subscriptions.ts +82 -0
- package/src/spoke/hub-tool-title.ts +89 -0
- package/src/spoke/hub-tool.ts +56 -18
- package/src/spoke/session-context.ts +10 -2
- package/src/spoke/spoke.ts +227 -237
- package/src/spoke/summary.ts +36 -1
- package/src/spoke/taskboard.ts +5 -6
- package/src/spoke/voice-front.ts +323 -30
- package/src/spoke/who-teams.ts +51 -2
- package/src/tui-messaging.ts +24 -10
- package/src/tui.ts +6 -6
- package/web/dist/assets/{ArtifactPage-BKISN-Qj.js → ArtifactPage-CsPjam8D.js} +1 -1
- package/web/dist/assets/{ArtifactPane-CKOiAUNH.js → ArtifactPane-B9weO8L6.js} +1 -1
- package/web/dist/assets/ArtifactWorkbench-DzKhMs3l.js +6 -0
- package/web/dist/assets/{Diagram-vdpmp3xu.css → Diagram-7y8kg3og.css} +1 -1
- package/web/dist/assets/Diagram-CuRIffka.js +1 -0
- package/web/dist/assets/{Library-CcjZXsfF.js → Library-rv5DGtrm.js} +2 -2
- package/web/dist/assets/Lightbox-BlbivCkd.js +1 -0
- package/web/dist/assets/MediaImage-Ch3ryIdR.js +4 -0
- package/web/dist/assets/ProfilesDestination-DUuQ9jmd.js +1 -0
- package/web/dist/assets/{TeamsPage-CDyjrK-I.js → TeamsPage-D_17MkvN.js} +1 -1
- package/web/dist/assets/{Watchtower-DYwoUWME.js → Watchtower-DlrNOJ7X.js} +2 -2
- package/web/dist/assets/{artifacts-DU9uRgs1.js → artifacts-But87Jfo.js} +1 -1
- package/web/dist/assets/index-bX8B6NLo.js +33 -0
- package/web/dist/assets/index-rW6LKQj1.css +1 -0
- package/web/dist/assets/{live-ycZ_aQZO.js → live-BGLA-peO.js} +2 -2
- package/web/dist/assets/{pane-layout-0RrLHGDr.js → pane-layout-BW0RdX5a.js} +1 -1
- package/web/dist/assets/{renderer2d-BIwbgRoX.js → renderer2d-DyANQ1WS.js} +1 -1
- package/web/dist/assets/{renderer3d-C1a7eYGQ.js → renderer3d-vYYKFAJq.js} +1 -1
- package/web/dist/assets/{safe-area-DHhPy61Q.js → safe-area-B2Bk__FK.js} +1 -1
- package/web/dist/assets/{source-BWlvaODa.js → source-C-De8L3s.js} +1 -1
- package/web/dist/assets/toast-CXTwTvDw.js +9 -0
- package/web/dist/assets/vad-worklet-BfxJhrXy.js +27 -0
- package/web/dist/index.html +6 -6
- package/web/dist/assets/ArtifactWorkbench-d7R1o-rs.js +0 -6
- package/web/dist/assets/Diagram-azw2B_QH.js +0 -1
- package/web/dist/assets/Lightbox-Du56MXGX.js +0 -1
- package/web/dist/assets/MediaImage-Ds_JUx3a.js +0 -4
- package/web/dist/assets/ProfilesDestination-DdzbzmeK.js +0 -1
- package/web/dist/assets/index-C3ci5UtY.css +0 -1
- package/web/dist/assets/index-LNMZCqgE.js +0 -36
- package/web/dist/assets/toast-Cuu2K0hx.js +0 -1
- /package/{web/src/voice → packages/voice-client/src}/turn/LICENSE +0 -0
package/src/spoke/hub-tool.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// phase 3): the only credential the hub lets a session act with, which the spoke never puts in any environment.
|
|
7
7
|
// toolIdentity picks the token per verb and never falls back from one kind to the other. Messages from other sessions are shown in the
|
|
8
8
|
// hub's envelope (packages/sdk/src/envelope.ts), sanitised, and marked as not coming from the user.
|
|
9
|
+
// Each result's metadata carries a one-line title (src/spoke/hub-tool-title.ts) that reads like an incoming notice.
|
|
10
|
+
import { withHubToolTitle } from "./hub-tool-title"
|
|
9
11
|
import {
|
|
10
12
|
HubClient,
|
|
11
13
|
HubError,
|
|
@@ -27,7 +29,8 @@ import {
|
|
|
27
29
|
type TeamIntakeStatusInput,
|
|
28
30
|
wakeSkipNote,
|
|
29
31
|
} from "../../packages/sdk/src/index.ts"
|
|
30
|
-
import { ME_LINE, reachText, whoSections } from "./who-teams"
|
|
32
|
+
import { ME_LINE, reachText, titleText, whoSections, YOU_MARK, youLine } from "./who-teams"
|
|
33
|
+
import { HUB_TOOL_SUBSCRIPTION_VERBS, SUBSCRIPTION_TOOL_DESCRIPTION, SUBSCRIPTION_TOOL_INPUT, subscriptionVerb } from "./hub-tool-subscriptions"
|
|
31
34
|
|
|
32
35
|
export const HUB_TOOL = "hub"
|
|
33
36
|
|
|
@@ -47,11 +50,13 @@ export const HUB_TOOL_SELF_VERBS = ["name", "role_claim", "role_release"] as con
|
|
|
47
50
|
* concierge_find looks the user's sessions up by @name or title words through GET /api/agent/sessions?q=, an ordinary
|
|
48
51
|
* read that any session (and the user) may make: it is a Concierge verb only for its framed, untrusted output. (FX-07)
|
|
49
52
|
* Text without a letter or digit is answered here without a request (the hub would match nothing).
|
|
53
|
+
* (Concierge grant requests) concierge_request_grant asks the user for a scoped grant (POST …/grant-requests: a tap
|
|
54
|
+
* Decision; the hub clamps it).
|
|
50
55
|
*/
|
|
51
|
-
export const HUB_TOOL_CONCIERGE_VERBS = ["concierge_board", "concierge_decisions", "concierge_decision", "concierge_notes", "concierge_notes_set", "concierge_watch", "concierge_unwatch", "concierge_mute", "concierge_unmute", "concierge_arm", "concierge_propose", "concierge_find", "concierge_form"] as const
|
|
56
|
+
export const HUB_TOOL_CONCIERGE_VERBS = ["concierge_board", "concierge_decisions", "concierge_decision", "concierge_notes", "concierge_notes_set", "concierge_watch", "concierge_unwatch", "concierge_mute", "concierge_unmute", "concierge_arm", "concierge_propose", "concierge_find", "concierge_form", "concierge_request_grant"] as const
|
|
52
57
|
export type HubToolConciergeVerb = (typeof HUB_TOOL_CONCIERGE_VERBS)[number]
|
|
53
58
|
|
|
54
|
-
export const HUB_TOOL_VERBS = ["msg", "ask", "reply", "decline", "inbox", "who", "status", "show", "whoami", "intake", "intake_update", ...HUB_TOOL_SELF_VERBS, ...HUB_TOOL_ACT_VERBS, ...HUB_TOOL_CONCIERGE_VERBS] as const
|
|
59
|
+
export const HUB_TOOL_VERBS = ["msg", "ask", "reply", "decline", "inbox", "who", "status", "show", "whoami", "intake", "intake_update", ...HUB_TOOL_SUBSCRIPTION_VERBS, ...HUB_TOOL_SELF_VERBS, ...HUB_TOOL_ACT_VERBS, ...HUB_TOOL_CONCIERGE_VERBS] as const
|
|
55
60
|
export type HubToolVerb = (typeof HUB_TOOL_VERBS)[number]
|
|
56
61
|
|
|
57
62
|
/** Which of the session's tokens a verb uses: "act" for act and Concierge verbs, "shell" for every other verb. */
|
|
@@ -75,8 +80,9 @@ export const HUB_TOOL_DESCRIPTION =
|
|
|
75
80
|
"msg (tell a session something: to, text; urgent steers into its running turn; wake:false does not start a turn), " +
|
|
76
81
|
"ask (ask a question: to, text; wait = seconds to block for the answer, at most 600; without wait the answer arrives later as a hub message), " +
|
|
77
82
|
"reply (answer a message: id, text), decline (decline an ask: id, text = the reason), " +
|
|
78
|
-
"inbox (open asks to you, your open asks, recent messages), who (
|
|
83
|
+
"inbox (open asks to you, your open asks, recent messages), who (sessions: online ones, and named or role-holding ones while offline, yours first and marked; project, role, team; grouped by team with your reach to each), " +
|
|
79
84
|
"status (machines, projects, sessions and what needs attention, with request IDs), show (one session: session; its state and blockers with the request and form IDs approve, deny and answer take), whoami (this session's hub identity). " +
|
|
85
|
+
SUBSCRIPTION_TOOL_DESCRIPTION +
|
|
80
86
|
"This session itself (never another): name (set your @name: name, e.g. @billing-lead; your previous name stays an alias for 24 h; a taken name fails with name_taken), " +
|
|
81
87
|
"role_claim (hold a role so @@role reaches you: role, project scopes it to a project, exclusive to be its only holder; reserved roles such as orchestrator are the user's to assign), role_release (role, project). " +
|
|
82
88
|
"Team intake, for the session in a team's entry role: intake (team = the team's ID; id = an intake ID for one item, else the list), " +
|
|
@@ -94,11 +100,11 @@ export const HUB_TOOL_INPUT = {
|
|
|
94
100
|
type: "object",
|
|
95
101
|
properties: {
|
|
96
102
|
verb: { type: "string", enum: [...HUB_TOOL_VERBS], description: "What to do." },
|
|
97
|
-
to: { type: "string", description: "msg/ask: the recipient address (@name, @@role, @@role#team, @name.project, ses_…, @me)." },
|
|
103
|
+
to: { type: "string", description: "msg/ask: the recipient address (@name, @@role, @@role#team, @name.project, ses_…, @me); subscribe: what to follow (a session, @@role, #team or children; several separated by commas)." },
|
|
98
104
|
text: { type: "string", description: "msg/ask/reply: the message text; decline: the reason; prompt: the prompt." },
|
|
99
|
-
id: { type: "string", description: "reply/decline: the ID of the message you answer (from its [hub] header); approve/deny: the permission request's ID; answer: the form's ID; intake/intake_update: the intake ID (itk_…)." },
|
|
105
|
+
id: { type: "string", description: "reply/decline: the ID of the message you answer (from its [hub] header); approve/deny: the permission request's ID; answer: the form's ID; intake/intake_update: the intake ID (itk_…); unsubscribe: the subscription's ID (sub_…)." },
|
|
100
106
|
wait: { type: "number", description: "ask: seconds to wait for the answer (0–600, default 0)." },
|
|
101
|
-
expires: { type: "number", description: "ask: seconds until the ask expires (the hub's default when unset)." },
|
|
107
|
+
expires: { type: "number", description: "ask: seconds until the ask expires (the hub's default when unset); subscribe: seconds until the subscription ends (default a day, at most 30 days)." },
|
|
102
108
|
urgent: { type: "boolean", description: "msg/ask: steer into the recipient's running turn." },
|
|
103
109
|
wake: { type: "boolean", description: "msg/ask/reply: false admits the message without starting a turn." },
|
|
104
110
|
all: { type: "boolean", description: "msg/ask: send to every holder of a role address (no wait)." },
|
|
@@ -109,7 +115,7 @@ export const HUB_TOOL_INPUT = {
|
|
|
109
115
|
why: { type: "string", description: "approve/deny/answer/prompt/stop: why (audited; deny passes it to the harness)." },
|
|
110
116
|
answer: { type: "object", description: "answer: the form's answer, field key → value (text, number, true/false or a list of texts)." },
|
|
111
117
|
cancel: { type: "boolean", description: "answer: dismiss the form instead of answering." },
|
|
112
|
-
delivery: { type: "string", enum: ["queue", "steer"], description: "prompt: how it enters a busy session." },
|
|
118
|
+
delivery: { type: "string", enum: ["queue", "steer", "silent"], description: "prompt: how it enters a busy session (queue or steer); subscribe: queue (default), steer or silent." },
|
|
113
119
|
project: { type: "string", description: "who: only this project; role_claim/role_release: the project the role is scoped to." },
|
|
114
120
|
role: { type: "string", description: "who: only holders of this role; role_claim/role_release: the role (reviewer or @@reviewer)." },
|
|
115
121
|
team: { type: "string", description: "who: only this team (one of yours, when you are in teams); intake/intake_update: the team's ID (team_…)." },
|
|
@@ -117,7 +123,14 @@ export const HUB_TOOL_INPUT = {
|
|
|
117
123
|
reason: { type: "string", description: "intake_update: why it failed or was cancelled (one line)." },
|
|
118
124
|
revision: { type: "number", description: "intake_update: the revision you read (optional: the current one is read first)." },
|
|
119
125
|
propose: { type: "string", enum: ["stop", "deny", "prompt", "answer"], description: "concierge_propose: the act to propose to the user." },
|
|
126
|
+
...SUBSCRIPTION_TOOL_INPUT,
|
|
120
127
|
recommend: { type: "boolean", description: "concierge_propose and approve: true only when YOU recommend doing it (shown as recommended, with why). Leave it out when the user only asked you to propose or try." },
|
|
128
|
+
projects: { type: "array", items: { type: "string" }, description: "concierge_request_grant: the watched projects (as your sessions name them) the grant covers." },
|
|
129
|
+
verbs: { type: "array", items: { type: "string" }, description: "concierge_request_grant: approve, deny, answer (prompt, stop only with userAsked)." },
|
|
130
|
+
permissions: { type: "array", items: { type: "string" }, description: "concierge_request_grant: the permissions approve may allow (read, edit, …)." },
|
|
131
|
+
never: { type: "array", items: { type: "string" }, description: "concierge_request_grant: extra never rules (the user's are added by the hub)." },
|
|
132
|
+
hours: { type: "number", description: "concierge_request_grant: how long the grant lasts after approval (default 8, at most 24)." },
|
|
133
|
+
userAsked: { type: "boolean", description: "concierge_request_grant: true only when the user explicitly asked for prompt or stop." },
|
|
121
134
|
},
|
|
122
135
|
required: ["verb"],
|
|
123
136
|
additionalProperties: false,
|
|
@@ -193,7 +206,8 @@ export async function runHubTool(spoke: () => HubToolSpoke | undefined, value: u
|
|
|
193
206
|
const secrets = [identity.token, toolIdentity(current, context.sessionID, kind === "act" ? "shell" : "act")?.token]
|
|
194
207
|
const redact = (result: HubToolResult): HubToolResult => secrets.reduce((out, token) => (token ? scrub(out, token) : out), result)
|
|
195
208
|
try {
|
|
196
|
-
|
|
209
|
+
// The title (metadata only) is redacted with the rest: the text it quotes is the agent's.
|
|
210
|
+
return redact(withHubToolTitle(input, await dispatch(client, verb as HubToolVerb, input, context)))
|
|
197
211
|
} catch (cause) {
|
|
198
212
|
if (cause instanceof InputError) return failure(cause.message, "invalid")
|
|
199
213
|
const needsUser = needsUserOf(cause)
|
|
@@ -255,20 +269,22 @@ async function dispatch(client: HubClient, verb: HubToolVerb, input: Input, cont
|
|
|
255
269
|
const answer = await client.who({ ...(project ? { project } : {}), ...(role ? { role } : {}), ...(team ? { team } : {}), ...signal })
|
|
256
270
|
const { sessions } = answer
|
|
257
271
|
const teams = (answer.teams ?? []).map((t) => ({ ...t, name: sanitizeLabel(t.name), yourRoles: (t.yourRoles ?? []).map((r) => sanitizeLabel(r)) }))
|
|
258
|
-
const rows = sessions.map((s) => ({ ...s, teams: (s.teams ?? []).map((t) => ({ ...t, team: sanitizeLabel(t.team) })) }))
|
|
259
|
-
const
|
|
260
|
-
|
|
272
|
+
const rows = sessions.map((s) => ({ ...s, ...(s.name ? { name: sanitizeLabel(s.name) } : {}), ...(s.project ? { project: sanitizeLabel(s.project) } : {}), teams: (s.teams ?? []).map((t) => ({ ...t, team: sanitizeLabel(t.team) })) }))
|
|
273
|
+
const meta = { ...(teams.length ? { teams: teams.length } : {}), ...(answer.scope ? { scope: answer.scope } : {}) }
|
|
274
|
+
if (!rows.length) return { content: "No sessions to show.", metadata: { verb, sessions: 0, ...meta } }
|
|
275
|
+
const you = context.sessionID
|
|
276
|
+
const lines = [youLine(rows.find((s) => s.sessionID === you), you, "the name verb sets one")]
|
|
277
|
+
for (const section of whoSections(rows, { teams, you, ...(answer.scope ? { scope: answer.scope } : {}) })) {
|
|
261
278
|
if (section.title) lines.push(section.title)
|
|
262
279
|
for (const { row: s, teamRoles, reach } of section.rows) {
|
|
263
280
|
const roles = (teamRoles ?? s.roles.map((r) => r.role)).map((r) => `@@${sanitizeLabel(r)}`).join(" ")
|
|
264
|
-
const
|
|
265
|
-
const parts = [s.name ?
|
|
281
|
+
const label = [s.name ? `@${s.name}` : s.sessionID, s.sessionID === you ? YOU_MARK : ""].filter(Boolean).join(" ")
|
|
282
|
+
const parts = [label, s.name ? s.sessionID : titleText(s.title ? sanitizeLabel(s.title) : undefined), s.project ?? "", s.state ?? "", s.online ? "online" : "offline", s.harness ? sanitizeLabel(s.harness) : "", roles, reachText(reach)]
|
|
266
283
|
lines.push((section.title ? " " : "") + parts.filter(Boolean).join(" · "))
|
|
267
284
|
}
|
|
268
285
|
}
|
|
269
|
-
if (answer.scope === "team"
|
|
270
|
-
|
|
271
|
-
return { content: lines.length ? lines.join("\n") : "No named or role-holding sessions.", metadata: { verb, sessions: sessions.length, ...meta } }
|
|
286
|
+
if (answer.scope === "team") lines.push(ME_LINE)
|
|
287
|
+
return { content: lines.join("\n"), metadata: { verb, sessions: sessions.length, ...meta } }
|
|
272
288
|
}
|
|
273
289
|
case "show": {
|
|
274
290
|
const addr = typeof input.session === "string" ? input.session.trim() : ""
|
|
@@ -312,6 +328,10 @@ async function dispatch(client: HubClient, verb: HubToolVerb, input: Input, cont
|
|
|
312
328
|
case "intake":
|
|
313
329
|
case "intake_update":
|
|
314
330
|
return intake(client, verb, input, signal)
|
|
331
|
+
case "subscribe":
|
|
332
|
+
case "unsubscribe":
|
|
333
|
+
case "subscriptions":
|
|
334
|
+
return subscriptionVerb(client, verb, input, signal)
|
|
315
335
|
case "name":
|
|
316
336
|
case "role_claim":
|
|
317
337
|
case "role_release":
|
|
@@ -329,6 +349,7 @@ async function dispatch(client: HubClient, verb: HubToolVerb, input: Input, cont
|
|
|
329
349
|
case "concierge_propose":
|
|
330
350
|
case "concierge_find":
|
|
331
351
|
case "concierge_form":
|
|
352
|
+
case "concierge_request_grant":
|
|
332
353
|
return conciergeVerb(client, verb, input, signal)
|
|
333
354
|
case "whoami": {
|
|
334
355
|
const { actor, user } = await client.whoami(signal)
|
|
@@ -499,7 +520,7 @@ function sent(verb: string, result: Sent): HubToolResult {
|
|
|
499
520
|
const { message } = result
|
|
500
521
|
return {
|
|
501
522
|
content: `${verb === "decline" ? "Declined" : verb === "reply" ? "Replied" : "Sent"} ${message.id} to ${partyText(message)} (${deliveryNote(result)}).`,
|
|
502
|
-
metadata: { verb, id: message.id, ...deliveryMetadata(result) },
|
|
523
|
+
metadata: { verb, id: message.id, to: partyText(message), ...deliveryMetadata(result) },
|
|
503
524
|
}
|
|
504
525
|
}
|
|
505
526
|
|
|
@@ -764,6 +785,23 @@ async function conciergeVerb(client: HubClient, verb: HubToolConciergeVerb, inpu
|
|
|
764
785
|
: "Nothing is armed."
|
|
765
786
|
return { content, metadata: { verb, decisionID: r.armed?.decisionID ?? null } }
|
|
766
787
|
}
|
|
788
|
+
case "concierge_request_grant": {
|
|
789
|
+
const list = (key: string, required: boolean) => {
|
|
790
|
+
const value = input[key]
|
|
791
|
+
if (value === undefined && !required) return {}
|
|
792
|
+
if (!Array.isArray(value) || (required && value.length === 0)) throw new InputError(`hub ${verb}: "${key}" is a list${required ? " (at least one)" : ""}`)
|
|
793
|
+
return { [key]: value }
|
|
794
|
+
}
|
|
795
|
+
const body: Record<string, unknown> = { ...list("projects", true), ...list("verbs", true), ...list("permissions", false), ...list("never", false) }
|
|
796
|
+
if (input.hours !== undefined) body.hours = input.hours
|
|
797
|
+
if (input.userAsked !== undefined) body.userAsked = input.userAsked
|
|
798
|
+
body.why = str(input, "why", verb)
|
|
799
|
+
const { decision } = await client.call<{ decision: { id: string; tier: string } }>("POST", `${CONCIERGE_PATH}/grant-requests`, { body, ...signal })
|
|
800
|
+
return {
|
|
801
|
+
content: `Requested the grant: Decision ${decision.id} (${decision.tier}) waits for the user's tap on the card in the Concierge panel. Do not retry and do not arm it; tell the user it waits for their tap.`,
|
|
802
|
+
metadata: { verb, decisionID: decision.id, tier: decision.tier },
|
|
803
|
+
}
|
|
804
|
+
}
|
|
767
805
|
case "concierge_propose": {
|
|
768
806
|
const propose = input.propose
|
|
769
807
|
if (propose !== "stop" && propose !== "deny" && propose !== "prompt" && propose !== "answer") throw new InputError(`hub ${verb}: "propose" is stop, deny, prompt or answer`)
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// LOCKED sessions (the Concierge's, R1 of the CON-04 verify): a context with lockAgent locks its session for good.
|
|
17
17
|
// A locked session, WHATEVER agent it runs on and with or without a context (cleared, or not re-sent yet after a
|
|
18
18
|
// restart), keeps only CONCIERGE_TOOLS (intersected with its context's allow-list), never gets conversation mode,
|
|
19
|
-
// voice style or
|
|
19
|
+
// voice style or a voice front. Locks survive `clear`, the context bound and a restart: they are persisted in
|
|
20
20
|
// the plugin's state dir (lockFile), so a restarted OpenCode locks the session before the hub reconnects. The agent
|
|
21
21
|
// a locked session was last seen on is reported to the hub (SessionInfo.agent), which suspends a Concierge found on
|
|
22
22
|
// another agent.
|
|
@@ -32,6 +32,8 @@ interface Context {
|
|
|
32
32
|
readonly tail: readonly string[]
|
|
33
33
|
readonly deny: readonly string[]
|
|
34
34
|
readonly allow?: ReadonlySet<string>
|
|
35
|
+
/** (P2) The compact board for the session's voice front (never in the session's own prompt). */
|
|
36
|
+
readonly frontBoard?: string
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
/** Parts at the end of the system prompt (stable between turns), in this order. */
|
|
@@ -277,7 +279,8 @@ export class SessionContexts {
|
|
|
277
279
|
const byKey = new Map(req.parts.map((p) => [p.key, p.text]))
|
|
278
280
|
const pick = (keys: readonly SessionContextKey[]) => keys.flatMap((key) => (byKey.has(key) ? [byKey.get(key)!] : []))
|
|
279
281
|
const allow = req.tools?.allow
|
|
280
|
-
|
|
282
|
+
const frontBoard = byKey.get("frontBoard")
|
|
283
|
+
this.contexts.set(req.sessionID, { revision: req.revision, parts: pick(SYSTEM_KEYS), tail: pick(TAIL_KEYS), deny: [...(req.tools?.deny ?? [])], ...(allow ? { allow: new Set(allow) } : {}), ...(frontBoard !== undefined ? { frontBoard } : {}) })
|
|
281
284
|
this.bound(this.contexts)
|
|
282
285
|
return { revision: req.revision }
|
|
283
286
|
}
|
|
@@ -301,6 +304,11 @@ export class SessionContexts {
|
|
|
301
304
|
return [...(this.contexts.get(sessionID)?.tail ?? [])]
|
|
302
305
|
}
|
|
303
306
|
|
|
307
|
+
/** (P2) The compact board the hub sent for the session's voice front (frontBoard part), if any. */
|
|
308
|
+
frontBoard(sessionID: string): string | undefined {
|
|
309
|
+
return this.contexts.get(sessionID)?.frontBoard
|
|
310
|
+
}
|
|
311
|
+
|
|
304
312
|
/** A Concierge session: only CONCIERGE_TOOLS, its agent and model kept, no conversation mode. */
|
|
305
313
|
locked(sessionID: string): boolean {
|
|
306
314
|
if (!this.locks.has(sessionID)) this.refresh()
|