@borgee/agents-host 0.2.33 → 0.2.44

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.
Files changed (60) hide show
  1. package/README.md +28 -7
  2. package/dist/agents-host.d.ts +19 -0
  3. package/dist/agents-host.js +163 -44
  4. package/dist/chat/chat-control-plane.d.ts +9 -0
  5. package/dist/chat/sdk-chat-control-plane.d.ts +10 -1
  6. package/dist/chat/sdk-chat-control-plane.js +9 -0
  7. package/dist/cli-args.d.ts +1 -1
  8. package/dist/cli-args.js +5 -0
  9. package/dist/compatibility-gates.d.ts +1 -0
  10. package/dist/compatibility-gates.js +2 -0
  11. package/dist/config.d.ts +4 -1
  12. package/dist/config.js +21 -3
  13. package/dist/context/injection.d.ts +5 -3
  14. package/dist/context/injection.js +45 -29
  15. package/dist/context/main-session-delegation.d.ts +1 -0
  16. package/dist/context/main-session-delegation.js +6 -0
  17. package/dist/context/prompt.js +25 -35
  18. package/dist/context/skill-manual.d.ts +13 -0
  19. package/dist/context/skill-manual.js +18 -0
  20. package/dist/context/turn-preparation.js +13 -4
  21. package/dist/gateway/localhost-gateway.js +75 -1
  22. package/dist/hosted-turn-content.d.ts +15 -0
  23. package/dist/hosted-turn-content.js +50 -0
  24. package/dist/local-config.js +10 -1
  25. package/dist/managed-daemon.d.ts +3 -2
  26. package/dist/managed-daemon.js +82 -29
  27. package/dist/plugin-sdk.js +58 -1
  28. package/dist/plugin-sdk.js.map +2 -2
  29. package/dist/policy/gateway-authorization.d.ts +18 -3
  30. package/dist/policy/gateway-authorization.js +33 -1
  31. package/dist/providers/claude/adapter.d.ts +3 -1
  32. package/dist/providers/claude/adapter.js +13 -1
  33. package/dist/providers/claude/cli-client.d.ts +36 -3
  34. package/dist/providers/claude/cli-client.js +225 -37
  35. package/dist/providers/codex/adapter.d.ts +3 -1
  36. package/dist/providers/codex/adapter.js +13 -1
  37. package/dist/providers/codex/cli-client.d.ts +35 -3
  38. package/dist/providers/codex/cli-client.js +212 -31
  39. package/dist/providers/codex/project-doc.js +16 -29
  40. package/dist/providers/copilot/adapter.d.ts +3 -1
  41. package/dist/providers/copilot/adapter.js +13 -1
  42. package/dist/providers/copilot/cli-client.d.ts +34 -2
  43. package/dist/providers/copilot/cli-client.js +196 -18
  44. package/dist/providers/create-provider.d.ts +1 -1
  45. package/dist/providers/create-provider.js +30 -14
  46. package/dist/providers/idle-backend-shutdown.d.ts +16 -0
  47. package/dist/providers/idle-backend-shutdown.js +53 -0
  48. package/dist/providers/provider-adapter.d.ts +35 -0
  49. package/dist/providers/provider-adapter.js +44 -1
  50. package/dist/state-paths.d.ts +9 -1
  51. package/dist/state-paths.js +22 -3
  52. package/dist/types.d.ts +40 -2
  53. package/package.json +2 -2
  54. package/skills/borgee-agent/SKILL.md +133 -35
  55. package/skills/borgee-agent/references/errors.md +38 -0
  56. package/skills/borgee-agent/references/task-properties.md +30 -0
  57. package/skills/borgee-agent/scripts/borgee-agent.mjs +553 -0
  58. package/skills/borgee-agent/scripts/borgee-agent.py +547 -0
  59. package/skills/borgee-agent/borgee-agent.mjs +0 -507
  60. package/skills/borgee-agent/borgee-agent.py +0 -438
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Idle shutdown policy for a provider's shared ACP adapter process.
3
+ *
4
+ * Every agent keeps one adapter child process resident across turns, so an
5
+ * agent nobody is talking to still pins a whole coding-CLI runtime in memory.
6
+ * The scheduler arms a timer whenever its owning client reports that no channel
7
+ * has work left, and hands the teardown back to the client once that timer
8
+ * expires; the client re-spawns the process lazily on the next turn.
9
+ *
10
+ * The `isIdle` probe is re-evaluated when the timer fires because a turn can
11
+ * arrive between arming and expiry, and the timer callback is the last point at
12
+ * which the shutdown can still be abandoned cheaply.
13
+ */
14
+ import { PROVIDER_IDLE_SHUTDOWN_COMPATIBILITY_GATE } from '../compatibility-gates.js';
15
+ export const IDLE_BACKEND_SHUTDOWN_DISABLED_MS = 0;
16
+ export function resolveIdleBackendShutdownMs(idleShutdownMinutes, compatibilityGates) {
17
+ if (!compatibilityGates.has(PROVIDER_IDLE_SHUTDOWN_COMPATIBILITY_GATE)) {
18
+ return IDLE_BACKEND_SHUTDOWN_DISABLED_MS;
19
+ }
20
+ return idleShutdownMinutes * 60 * 1000;
21
+ }
22
+ export class IdleBackendShutdownScheduler {
23
+ options;
24
+ timer;
25
+ constructor(options) {
26
+ this.options = options;
27
+ }
28
+ get enabled() {
29
+ return this.options.idleShutdownMs > IDLE_BACKEND_SHUTDOWN_DISABLED_MS;
30
+ }
31
+ cancel() {
32
+ if (!this.timer) {
33
+ return;
34
+ }
35
+ clearTimeout(this.timer);
36
+ this.timer = undefined;
37
+ }
38
+ reconcile() {
39
+ this.cancel();
40
+ if (!this.enabled || !this.options.isIdle()) {
41
+ return;
42
+ }
43
+ this.timer = setTimeout(() => {
44
+ this.timer = undefined;
45
+ if (!this.options.isIdle()) {
46
+ return;
47
+ }
48
+ void this.options.shutdown().catch((error) => {
49
+ this.options.onShutdownFailed(error);
50
+ });
51
+ }, this.options.idleShutdownMs);
52
+ }
53
+ }
@@ -1,5 +1,40 @@
1
1
  import type { ProviderGenerateOptions, ProviderInput, ProviderReply } from '../types.js';
2
+ export type ProviderImageInputSupport = 'supported' | 'metadata-only' | 'not-supported';
3
+ export type ProviderImageTransportSupport = 'supported' | 'not-supported';
4
+ export type ProviderImageTransportSource = 'transport-derived' | 'runtime-derived';
5
+ export type ProviderImageTransportDelivery = 'host-materialized-local-path' | 'blob';
6
+ export type ProviderImageInputReason = 'real-media-delivered' | 'shared-host-attachment-metadata-fallback' | 'host-policy-blocked';
7
+ export type ProviderImageTransportReason = 'real-media-delivered' | 'transport-does-not-deliver-real-media' | 'runtime-capability-not-yet-resolved' | 'host-policy-blocked';
8
+ export interface SharedHostAttachmentMetadataFallbackCapability {
9
+ support: 'metadata-only';
10
+ delivery: 'prompt-text-attachment-metadata';
11
+ }
12
+ export interface ProviderImageTransportCapability {
13
+ source: ProviderImageTransportSource;
14
+ support: ProviderImageTransportSupport;
15
+ reason: ProviderImageTransportReason;
16
+ delivery: readonly ProviderImageTransportDelivery[];
17
+ maxImageCount?: number;
18
+ maxBytesPerImage?: number;
19
+ maxTotalBytes?: number;
20
+ }
21
+ export interface ProviderImageCapability {
22
+ support: ProviderImageInputSupport;
23
+ reason: ProviderImageInputReason;
24
+ sharedHostFallback: SharedHostAttachmentMetadataFallbackCapability;
25
+ transport: ProviderImageTransportCapability;
26
+ }
27
+ export interface ProviderCapabilities {
28
+ imageInput: ProviderImageCapability;
29
+ }
30
+ export declare const SHARED_HOST_ATTACHMENT_METADATA_FALLBACK: SharedHostAttachmentMetadataFallbackCapability;
31
+ export declare function createHostedProviderCapabilities(params: {
32
+ imageInputTransport: ProviderImageTransportCapability;
33
+ sharedHostFallback?: SharedHostAttachmentMetadataFallbackCapability;
34
+ }): ProviderCapabilities;
35
+ export declare const TEXT_ONLY_HOSTED_PROVIDER_CAPABILITIES: ProviderCapabilities;
2
36
  export interface ProviderAdapter {
37
+ readonly capabilities: ProviderCapabilities;
3
38
  generateReply(input: ProviderInput, options?: ProviderGenerateOptions): Promise<ProviderReply>;
4
39
  dispose?(): Promise<void>;
5
40
  }
@@ -1 +1,44 @@
1
- export {};
1
+ export const SHARED_HOST_ATTACHMENT_METADATA_FALLBACK = Object.freeze({
2
+ support: 'metadata-only',
3
+ delivery: 'prompt-text-attachment-metadata',
4
+ });
5
+ function resolveImageInputSupport(transport, sharedHostFallback) {
6
+ if (transport.support === 'supported') {
7
+ return {
8
+ support: 'supported',
9
+ reason: 'real-media-delivered',
10
+ };
11
+ }
12
+ if (transport.reason === 'host-policy-blocked') {
13
+ return {
14
+ support: 'not-supported',
15
+ reason: 'host-policy-blocked',
16
+ };
17
+ }
18
+ return {
19
+ support: sharedHostFallback.support,
20
+ reason: 'shared-host-attachment-metadata-fallback',
21
+ };
22
+ }
23
+ export function createHostedProviderCapabilities(params) {
24
+ const sharedHostFallback = params.sharedHostFallback ?? SHARED_HOST_ATTACHMENT_METADATA_FALLBACK;
25
+ const transport = Object.freeze({
26
+ ...params.imageInputTransport,
27
+ delivery: Object.freeze([...params.imageInputTransport.delivery]),
28
+ });
29
+ return Object.freeze({
30
+ imageInput: Object.freeze({
31
+ ...resolveImageInputSupport(transport, sharedHostFallback),
32
+ sharedHostFallback,
33
+ transport,
34
+ }),
35
+ });
36
+ }
37
+ export const TEXT_ONLY_HOSTED_PROVIDER_CAPABILITIES = createHostedProviderCapabilities({
38
+ imageInputTransport: {
39
+ source: 'transport-derived',
40
+ support: 'not-supported',
41
+ reason: 'transport-does-not-deliver-real-media',
42
+ delivery: [],
43
+ },
44
+ });
@@ -1,4 +1,11 @@
1
1
  export declare function normalizeManagedRuntimeKey(serverUrl: string): string;
2
+ export type ManagedDaemonEndpoint = {
3
+ kind: 'named-pipe';
4
+ address: string;
5
+ } | {
6
+ kind: 'unix-socket';
7
+ address: string;
8
+ };
2
9
  export declare function resolveSingleAgentStateRoot(env?: NodeJS.ProcessEnv, resolvedHomeDir?: string, agentKey?: string): string;
3
10
  export declare function resolveManagedRuntimeRoot(serverUrl: string, env?: NodeJS.ProcessEnv, resolvedHomeDir?: string): string;
4
11
  /**
@@ -7,7 +14,8 @@ export declare function resolveManagedRuntimeRoot(serverUrl: string, env?: NodeJ
7
14
  */
8
15
  export declare function resolveUpdateCheckCachePath(env?: NodeJS.ProcessEnv, resolvedHomeDir?: string): string;
9
16
  export declare function resolveManagedStateRoot(rootPath: string): string;
10
- export declare function resolveManagedDaemonSocketPath(rootPath: string): string;
17
+ export declare function resolveManagedDaemonEndpoint(rootPath: string, platform?: NodeJS.Platform): ManagedDaemonEndpoint;
18
+ export declare function resolveManagedDaemonSocketPath(rootPath: string, platform?: NodeJS.Platform): string;
11
19
  export declare function resolveManagedDaemonLogPath(rootPath: string): string;
12
20
  export declare function resolveManagedRuntimeSettingsPath(rootPath: string): string;
13
21
  export declare function resolveManagedBootstrapLockPath(rootPath: string): string;
@@ -1,12 +1,13 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { homedir } from 'node:os';
3
- import { dirname, join, resolve } from 'node:path';
3
+ import { dirname, join, posix, resolve, win32 } from 'node:path';
4
4
  const SINGLE_AGENT_HOME_ROOT = '.borgee';
5
5
  const AGENTS_HOST_ROOT = 'agents-host';
6
6
  const SINGLE_AGENT_NAMESPACE = 'single-agent';
7
7
  const MANAGED_RUNTIME_NAMESPACE = 'managed';
8
8
  const MANAGED_STATE_DIRNAME = '.state';
9
9
  const MANAGED_DAEMON_SOCKET_FILENAME = 'ctl.sock';
10
+ const MANAGED_DAEMON_PIPE_PREFIX = '\\\\.\\pipe\\borgee-agents-host-managed-';
10
11
  const MANAGED_DAEMON_LOG_FILENAME = 'daemon.log';
11
12
  const MANAGED_RUNTIME_SETTINGS_FILENAME = 'managed-runtime-settings.json';
12
13
  const MANAGED_BOOTSTRAP_LOCK_DIRNAME = '.bootstrap.lock';
@@ -95,8 +96,26 @@ export function resolveUpdateCheckCachePath(env = process.env, resolvedHomeDir =
95
96
  export function resolveManagedStateRoot(rootPath) {
96
97
  return join(resolve(rootPath), MANAGED_STATE_DIRNAME);
97
98
  }
98
- export function resolveManagedDaemonSocketPath(rootPath) {
99
- return join(resolve(rootPath), MANAGED_DAEMON_SOCKET_FILENAME);
99
+ export function resolveManagedDaemonEndpoint(rootPath, platform = process.platform) {
100
+ const pathApi = platform === 'win32' ? win32 : posix;
101
+ const resolvedRoot = pathApi.resolve(rootPath);
102
+ if (platform === 'win32') {
103
+ const rootHash = createHash('sha256')
104
+ .update(resolvedRoot.toLowerCase())
105
+ .digest('hex')
106
+ .slice(0, 24);
107
+ return {
108
+ kind: 'named-pipe',
109
+ address: `${MANAGED_DAEMON_PIPE_PREFIX}${rootHash}`,
110
+ };
111
+ }
112
+ return {
113
+ kind: 'unix-socket',
114
+ address: posix.join(resolvedRoot, MANAGED_DAEMON_SOCKET_FILENAME),
115
+ };
116
+ }
117
+ export function resolveManagedDaemonSocketPath(rootPath, platform = process.platform) {
118
+ return resolveManagedDaemonEndpoint(rootPath, platform).address;
100
119
  }
101
120
  export function resolveManagedDaemonLogPath(rootPath) {
102
121
  return join(resolve(rootPath), MANAGED_DAEMON_LOG_FILENAME);
package/dist/types.d.ts CHANGED
@@ -7,10 +7,14 @@ export interface ProviderCommandConfig {
7
7
  copilotCommand: string;
8
8
  copilotArgs: string[];
9
9
  copilotSessionTtlMinutes: number;
10
+ /** Minutes a provider's shared ACP adapter process may stay idle before it is shut down; `0` keeps it resident. */
11
+ providerIdleShutdownMinutes: number;
10
12
  }
11
13
  export interface ProviderRuntimeConfig extends ProviderCommandConfig {
12
14
  provider: ProviderKind;
15
+ borgeeBaseUrl: string;
13
16
  stateRootDir: string;
17
+ agentApiKey: string;
14
18
  resolveStableAgentId?: () => string | undefined;
15
19
  }
16
20
  export interface HostedAgentConfig {
@@ -94,6 +98,7 @@ export interface ChannelMessageEvent {
94
98
  content?: string;
95
99
  body?: string;
96
100
  content_type?: string;
101
+ attachments?: HostedMessageAttachment[];
97
102
  created_at?: number;
98
103
  [key: string]: unknown;
99
104
  }
@@ -113,11 +118,32 @@ export interface ChannelHistoryEntry {
113
118
  authorId: string;
114
119
  body: string;
115
120
  contentType?: string;
121
+ attachments?: HostedMessageAttachment[];
116
122
  type?: string;
117
123
  replyToId?: string;
118
124
  createdAt: number;
119
125
  editedAt?: number;
120
126
  }
127
+ export interface HostedMessageAttachment {
128
+ url: string;
129
+ filename?: string;
130
+ contentType: string;
131
+ kind?: 'image' | 'file';
132
+ sizeBytes?: number;
133
+ }
134
+ export interface HostedTextContentPart {
135
+ type: 'text';
136
+ text: string;
137
+ }
138
+ export interface HostedImageContentPart {
139
+ type: 'image';
140
+ attachment: HostedMessageAttachment;
141
+ }
142
+ export interface HostedFileContentPart {
143
+ type: 'file';
144
+ attachment: HostedMessageAttachment;
145
+ }
146
+ export type HostedTurnContentPart = HostedTextContentPart | HostedImageContentPart | HostedFileContentPart;
121
147
  export interface ReadChannelHistoryInput {
122
148
  channelId: string;
123
149
  before?: number;
@@ -232,6 +258,7 @@ export interface ProviderInput {
232
258
  channelId: string;
233
259
  incomingAuthorId: string;
234
260
  incomingContent: string;
261
+ incomingParts?: HostedTurnContentPart[];
235
262
  incomingEventKind?: string;
236
263
  incomingMessageType?: string;
237
264
  collaboration?: ProviderCollaborationContext;
@@ -289,7 +316,7 @@ export interface TaskWorkspaceContext {
289
316
  }
290
317
  export interface PreparedPromptContext {
291
318
  channelContextPayloadPath?: string;
292
- gatewayAuthPath?: string;
319
+ gatewayCredentialPath?: string;
293
320
  collaborationTurnExecutionId?: string;
294
321
  collaborationTurnMode?: ProviderCollaborationTurnMode;
295
322
  collaborationOutcome?: CollaborationOutcomeSnapshot;
@@ -311,6 +338,8 @@ export interface PreparedProviderSessionRouting {
311
338
  }
312
339
  export interface PreparedProviderTurnInput {
313
340
  channelId: string;
341
+ incomingContent: string;
342
+ incomingParts: HostedTurnContentPart[];
314
343
  incomingEventKind?: string;
315
344
  incomingMessageType?: string;
316
345
  prompt: string;
@@ -342,6 +371,11 @@ export interface ProviderReply {
342
371
  awaitingUser?: ProviderAwaitingUser;
343
372
  control?: ProviderTurnControl;
344
373
  controlMalformed?: boolean;
374
+ /**
375
+ * The provider session this turn ran in, when the provider exposes one.
376
+ * agents-host records it on the task as agent.session_id.
377
+ */
378
+ sessionId?: string;
345
379
  }
346
380
  export interface PostedMessage {
347
381
  messageId: string;
@@ -355,7 +389,7 @@ export interface Task {
355
389
  id: string;
356
390
  channelId: string;
357
391
  guildId: string;
358
- seq?: number;
392
+ seq: number;
359
393
  title: string;
360
394
  description: string;
361
395
  status: string;
@@ -365,6 +399,10 @@ export interface Task {
365
399
  threadId: string | null;
366
400
  createdAt: number;
367
401
  updatedAt: number;
402
+ heartbeatIntervalMs: number;
403
+ heartbeatPrompt: string;
404
+ participants: string[];
405
+ properties: Record<string, string>;
368
406
  }
369
407
  export interface CreateTaskInput {
370
408
  channelId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@borgee/agents-host",
3
- "version": "0.2.33",
3
+ "version": "0.2.44",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -35,7 +35,7 @@
35
35
  "tsx": "^4.20.5",
36
36
  "typescript": "^5.9.3",
37
37
  "vitest": "^4.1.5",
38
- "@borgee/plugin-sdk": "0.2.5"
38
+ "@borgee/plugin-sdk": "0.4.0"
39
39
  },
40
40
  "scripts": {
41
41
  "predev": "pnpm --filter @borgee/plugin-sdk build",
@@ -1,35 +1,133 @@
1
- # Borgee Agent Skill Bootstrap
2
-
3
- This skill runtime surface stays read-only for final visible replies in this slice.
4
-
5
- Use one of the packaged local CLIs to inspect the current channel bootstrap payload. When `localhost-gateway` is enabled together with `context-injection` and `skill-runtime`, the same CLIs can also call the loopback gateway using the persisted context payload.
6
-
7
- - Node: `node ./borgee-agent.mjs --context /absolute/path/to/context.json --print-bootstrap`
8
- - Python: `python3 ./borgee-agent.py --context /absolute/path/to/context.json --print-bootstrap`
9
- - Node health: `node ./borgee-agent.mjs --context /absolute/path/to/context.json --auth-path /absolute/path/to/.localhost-gateway-auth.json --health`
10
- - Node bootstrap: `node ./borgee-agent.mjs --context /absolute/path/to/context.json --auth-path /absolute/path/to/.localhost-gateway-auth.json --read-bootstrap`
11
- - Node identity: `node ./borgee-agent.mjs --context /absolute/path/to/context.json --auth-path /absolute/path/to/.localhost-gateway-auth.json --get-me`
12
- - Node history: `node ./borgee-agent.mjs --context /absolute/path/to/context.json --auth-path /absolute/path/to/.localhost-gateway-auth.json --read-history --limit 20`
13
- - Node private draft snapshot: `node ./borgee-agent.mjs --context /absolute/path/to/context.json --auth-path /absolute/path/to/.localhost-gateway-auth.json --turn-execution-id turn-id --read-draft`
14
- - Node users: `node ./borgee-agent.mjs --context /absolute/path/to/context.json --auth-path /absolute/path/to/.localhost-gateway-auth.json --list-users`
15
- - Node auxiliary mention: `node ./borgee-agent.mjs --context /absolute/path/to/context.json --auth-path /absolute/path/to/.localhost-gateway-auth.json --turn-execution-id turn-id --send-mention user-id --body "Need review from <@user-id>"`
16
- - Node auxiliary reply: `node ./borgee-agent.mjs --context /absolute/path/to/context.json --auth-path /absolute/path/to/.localhost-gateway-auth.json --turn-execution-id turn-id --send-message --body "Following up here" --reply-to message-id`
17
- - Python health: `python3 ./borgee-agent.py --context /absolute/path/to/context.json --auth-path /absolute/path/to/.localhost-gateway-auth.json --health`
18
- - Python bootstrap: `python3 ./borgee-agent.py --context /absolute/path/to/context.json --auth-path /absolute/path/to/.localhost-gateway-auth.json --read-bootstrap`
19
- - Python identity: `python3 ./borgee-agent.py --context /absolute/path/to/context.json --auth-path /absolute/path/to/.localhost-gateway-auth.json --get-me`
20
- - Python history: `python3 ./borgee-agent.py --context /absolute/path/to/context.json --auth-path /absolute/path/to/.localhost-gateway-auth.json --read-history --limit 20`
21
- - Python private draft snapshot: `python3 ./borgee-agent.py --context /absolute/path/to/context.json --auth-path /absolute/path/to/.localhost-gateway-auth.json --turn-execution-id turn-id --read-draft`
22
- - Python users: `python3 ./borgee-agent.py --context /absolute/path/to/context.json --auth-path /absolute/path/to/.localhost-gateway-auth.json --list-users`
23
- - Python auxiliary mention: `python3 ./borgee-agent.py --context /absolute/path/to/context.json --auth-path /absolute/path/to/.localhost-gateway-auth.json --turn-execution-id turn-id --send-mention user-id --body "Need review from <@user-id>"`
24
- - Python auxiliary reply: `python3 ./borgee-agent.py --context /absolute/path/to/context.json --auth-path /absolute/path/to/.localhost-gateway-auth.json --turn-execution-id turn-id --send-message --body "Following up here" --reply-to message-id`
25
-
26
- The same CLIs also expose the task commands. Which ones apply depends on where the turn runs, which the injected `context.json` reports through `taskAssignmentContext`:
27
-
28
- - Parent channel (no `taskAssignmentContext.active`): `--create-task --title ...`, `--list-tasks`, `--get-task --task-id ...`, `--update-task --task-id ...`, `--read-task-history --task-id ...`
29
- - Task-assignment thread (`taskAssignmentContext.active: true`): `--get-task` and `--update-task` may omit `--task-id` and resolve the current thread task through the persisted `currentTaskId` or an agents-host local fallback; `--read-task-history --task-id ...` still works for that thread's own task, while `--create-task` and `--list-tasks` stay disabled and must be run from the parent channel
30
-
31
- `--read-task-history` reads the messages inside a task's thread and always requires an explicit `--task-id`; inside that task's own thread `--read-history` already reads the same messages, so the turn prompt offers the command in the parent channel only. It accepts the same `--limit` / `--before` / `--after` window as `--read-history` and answers the same not-found error for a task outside the current channel as for a task that does not exist.
32
-
33
- The private draft snapshot is read-only, collaboration-scoped, and separate from ordinary public channel messages.
34
- Auxiliary sends are only for short targeted escalation, reply-thread nudges, or mentions. They must not be used for the main final answer body, which still belongs to AgentsHost.
35
- Both CLIs are local-only, may only access the loopback gateway described above, and must not mutate files.
1
+ ---
2
+ name: borgee-agent
3
+ description: Read the Borgee channel this turn is running in and act on its tasks — channel history, visible participants, task list/create/get/update, task properties, and short auxiliary mentions — through the packaged local CLI. Use when you need to know what was said in this channel, who is here, or what the current task is, and when you need to record task state or ping another participant.
4
+ ---
5
+
6
+ # Borgee channel agent
7
+
8
+ This turn is running as an agent inside a Borgee channel. The packaged CLI is the only way to see that channel and to act on its tasks: it talks to a loopback-only gateway that is already authorized for this channel and this turn. This file is the whole manual — the invocation, every command, what is reachable when, and the limits.
9
+
10
+ ## Authorization
11
+
12
+ Every command below is already authorized on a turn whose prompt names a gateway credential file. Run it directly. Never ask the user for permission first, and never end a turn having asked to read instead of reading.
13
+
14
+ ## When to use it
15
+
16
+ - You are asked what was said here, who is here, or what you are supposed to be working on: read it, do not guess.
17
+ - You have made progress worth recording on the task: a status change, or a property such as the pull request that implements it.
18
+
19
+ ## When not to use it
20
+
21
+ - For your final answer. The host posts your turn's reply itself; sending it again through this CLI double-posts it.
22
+ - To find the gateway credential file. The absolute path is in this turn's prompt; the CLI never searches for it, and a file found by searching is not trusted.
23
+
24
+ ## Invocation
25
+
26
+ `scripts/borgee-agent.mjs` and `scripts/borgee-agent.py`, both in the `scripts/` directory beside this file, are the same tool: they accept the same grammar and print the same bytes. Neither is marked executable, so name the interpreter your host has and give the script its absolute path — this file's own directory plus `scripts/`:
27
+
28
+ ```
29
+ node <this file's directory>/scripts/borgee-agent.mjs --gateway <absolute path> <command> [arguments]
30
+ python3 <this file's directory>/scripts/borgee-agent.py --gateway <absolute path> <command> [arguments]
31
+ ```
32
+
33
+ Everything below writes that leading interpreter and script path as `borgee-agent`.
34
+
35
+ `--gateway` takes the absolute path this turn's prompt gives you. That one file is the whole handoff: which channel you are in, where the gateway listens, and the token that authorizes you. The token is rotated every turn, so read the path out of the current prompt rather than reusing one you remember.
36
+
37
+ ## Commands
38
+
39
+ | Command | What you get |
40
+ | --- | --- |
41
+ | `health` | Reachability. The only command that answers without the token, so it tells a dead gateway from a rotated one. |
42
+ | `bootstrap` | The channel context the host published for this turn: the channel id, whether collaboration is live, and inside a task thread the current task id. The one command whose answer you cannot predict. |
43
+ | `whoami` | This agent's own id and display name. |
44
+ | `history [--limit <n>] [--before <n>] [--after <n>]` | Recent messages in this channel. At most 20 per call — a larger `--limit` is silently reduced to 20, so page with `--before` rather than asking for more. `--before` and `--after` are `createdAt` epoch-millisecond cursors copied from a message you already hold, not message ids and not counts. |
45
+ | `users` | Every participant you share a channel, DM or thread with. That is a superset of this channel's members, not its roster. |
46
+ | `draft --turn-execution-id <id>` | This turn's host-private in-flight draft. It exists only once this turn has produced visible reply text, and answers `not_found` before that. |
47
+ | `send --body <text> --turn-execution-id <id> (--reply-to <message-id> \| --mention <user-id>)...` | Post a short auxiliary message. It must address someone. |
48
+ | `mention <user-id> --body <text> --turn-execution-id <id> [--reply-to <message-id>]` | Post a short auxiliary message addressed to one participant. |
49
+ | `task list` | Tasks in this channel. Parent channel only. |
50
+ | `task create --title <text> [--description <text>] [--assignee-id <user-id>]` | Create a task. Parent channel only. |
51
+ | `task get [<task-id>]` | One task with its properties. |
52
+ | `task update [<task-id>] [--status <open\|in_progress\|in_review\|done\|cancelled>] [--title <text>] [--description <text>] [--assignee-id <user-id>]` | Update the task. At least one field is required. |
53
+ | `task history <task-id> [--limit <n>] [--before <n>] [--after <n>]` | Messages in that task's thread. Same 20-per-call cap and the same `createdAt` cursors as `history`. |
54
+ | `task set-property [<task-id>] --key <key> --value <value>` | Set one task property. |
55
+ | `task delete-property [<task-id>] --key <key>` | Remove one task property. |
56
+
57
+ On `task get`, `task update`, `task set-property` and `task delete-property` the task id is an optional leading positional. Inside a task thread, omitting it addresses this thread's own task: the host's published binding names it, and where the host published none the gateway scans the parent channel's visible tasks for this thread instead — so pass the id explicitly when that scan cannot land on a single task. `task list` and `task create` are refused inside a task thread; they belong to the parent channel. In a parent channel every one of those ids is required, and omitting it answers `not_found`.
58
+
59
+ The keys `task set-property` and `task delete-property` accept are a closed set, and a value has a size limit: see `references/task-properties.md`.
60
+
61
+ ## What is available on a turn
62
+
63
+ `health`, `bootstrap`, `whoami`, `history` and the task commands are live on every turn whose prompt names a gateway credential file.
64
+
65
+ `users`, `draft`, `send` and `mention` are live only where collaboration is enabled; the prompt says when it is not, and the gateway answers `not_found` for all four. `draft`, `send` and `mention` additionally need the turn execution id the prompt carries: no command returns that id and the gateway credential file does not hold it.
66
+
67
+ A `not_found` from `draft` therefore has two readings — collaboration is off, or the host holds no draft for this turn yet — so take it as an answer about the draft, not as evidence that `users`, `send` and `mention` have gone.
68
+
69
+ Whether you are in a parent channel or inside a task assignment thread is in the prompt too, and it decides which task grammar above applies.
70
+
71
+ ## Inside a task thread
72
+
73
+ The assigned work belongs to that thread, and your ordinary final reply is the completion report.
74
+
75
+ - `task update --status in_progress` when you start.
76
+ - `task update --status in_review` when you finish.
77
+ - Never `send` or `mention` the completion report. Those are for intentional targeted escalation or cross-channel notification.
78
+
79
+ ## Talking to another participant
80
+
81
+ Asked to mention, ping, notify, or send a short note to someone visible: run `mention` yourself, then confirm what you sent. What notifies them is the visible `<@targetId>` token in the body — naming someone in prose alone does not reach them — and the CLI appends that token when your body leaves it out. If the user says "the other agent" and only one other agent is visible, resolve that id with `users` first.
82
+
83
+ Wanting to collaborate with another agent is the same act — send the short mention or reply-thread note yourself. There is no host-orchestrated protocol to ask for.
84
+
85
+ ## Errors
86
+
87
+ A failure prints one `error: …` line on stderr. Exit `2` is a wrong invocation: the request was never sent, so nothing changed.
88
+
89
+ `references/errors.md` maps every gateway error to its cause and its correction, and holds the exit-code contract.
90
+
91
+ ## A worked turn
92
+
93
+ You are running in a task thread, you have opened the pull request that implements the task, and you want the reviewer to know.
94
+
95
+ ```
96
+ $ borgee-agent --gateway /state/channels/channel-a/.borgee-agent-gateway.json task get
97
+ {
98
+ "id": "task-42",
99
+ "title": "Deliver the daily digest",
100
+ "status": "open",
101
+ "assigneeId": "agent-1",
102
+ "properties": {}
103
+ }
104
+
105
+ $ borgee-agent --gateway /state/channels/channel-a/.borgee-agent-gateway.json \
106
+ task set-property --key link.pr --value https://github.com/org/repo/pull/12
107
+ {
108
+ "id": "task-42",
109
+ "properties": { "link.pr": "https://github.com/org/repo/pull/12" }
110
+ }
111
+
112
+ $ borgee-agent --gateway /state/channels/channel-a/.borgee-agent-gateway.json \
113
+ task update --status in_progress
114
+ {
115
+ "id": "task-42",
116
+ "status": "in_progress"
117
+ }
118
+
119
+ $ borgee-agent --gateway /state/channels/channel-a/.borgee-agent-gateway.json \
120
+ mention user-7 --body "Digest PR is up for review" --turn-execution-id turn-9f3
121
+ {
122
+ "id": "message-311",
123
+ "body": "Digest PR is up for review <@user-7>"
124
+ }
125
+ ```
126
+
127
+ ## Constraints
128
+
129
+ - Local only. The CLI reaches the loopback gateway and nothing else.
130
+ - It does not read or write files anywhere except the one gateway credential file it is handed.
131
+ - Auxiliary sends are short notices — one line, at most twelve words counting the `<@id>` the CLI appends, addressed to someone or attached to a message. They are not a place for the answer.
132
+ - Exactly one auxiliary send per turn, whoever it addresses. A send the gateway rejects does not spend it.
133
+ - The draft is host-private. Read it to see what the host is about to post; never re-post it.
@@ -0,0 +1,38 @@
1
+ # Errors
2
+
3
+ ## Exit codes
4
+
5
+ | Code | Meaning |
6
+ | --- | --- |
7
+ | `0` | The command succeeded; its JSON is on stdout. |
8
+ | `1` | The gateway refused the request or could not be reached. The message carries the HTTP status and the gateway's JSON body. |
9
+ | `2` | A wrong invocation; the message names it. Nothing was sent. |
10
+
11
+ ## Gateway errors
12
+
13
+ | Status and error | Cause | Correction |
14
+ | --- | --- | --- |
15
+ | 401 `missing_or_invalid_token`, 401 `invalid_token` | The gateway credential file is from an earlier turn; its token has been rotated away. | Re-read the credential path this turn's prompt gives you. |
16
+ | 403 `channel_mismatch` | The task or channel is outside the channel this turn is bound to. | Only this channel is reachable. Work from `task list` in this channel. |
17
+ | 403 `permission_denied` | This agent is not allowed to perform that action in this channel. | Report it; do not retry the same call. |
18
+ | 403 `collaboration_not_enabled` | An auxiliary send on a turn where collaboration is off. | Do not send; put what you wanted to say in your final answer. |
19
+ | 403 `protocol_managed_turn` | An auxiliary send during a host-managed collaboration turn. | The host delivers this turn's reply. Do not send yourself. |
20
+ | 409 `stale_turn_execution_id` | The turn execution id belongs to an earlier turn, or the turn it names has already ended. | Use the turn execution id this turn's prompt carries; never one you remember. |
21
+ | 429 `collaboration_quota_exceeded` | This turn has already spent its one auxiliary send. | Say the rest in your final answer. |
22
+ | 429 `collaboration_target_cooldown` | The same reply target or mention set was addressed moments ago. | Do not repeat it. |
23
+ | 404 `not_found` on `task get` / `task update` / a property command with no task id | You are not inside a task thread, so there is no current task to resolve. | Pass the task id. |
24
+ | 404 `not_found` on a task command with a task id | The task does not exist, or belongs to another channel. | Check the id with `task list`. |
25
+ | 404 `not_found` on `users`, `send`, `mention` | Collaboration is not enabled for this turn, so those commands do not exist. | Do not use them; the turn prompt says when they are live. |
26
+ | 404 `not_found` on `draft` | Either collaboration is not enabled for this turn, or the host holds no draft for it yet — a draft exists only once the turn has produced visible reply text. | Not a verdict on the other collaboration commands: `users`, `send` and `mention` may well answer on this same turn. Carry on and read the draft later if you still need it. |
27
+ | 404 `bootstrap_unavailable` | The host has not published this turn's channel payload yet. | Retry the read once; if it persists, continue without it. |
28
+ | 400 `task_thread_collection_not_allowed` | `task list` or `task create` inside a task thread. | Those belong to the parent channel. |
29
+ | 400 `multiline_message_body_not_allowed`, `message_body_too_verbose` | An auxiliary send must be one line of at most 12 words, and the `<@id>` the CLI appends counts as one of them — each extra `--mention` costs another. | Shorten it to a single-line notice. |
30
+ | 400 `missing_reply_or_mentions` | A send that addresses nobody: no `--reply-to` and no visible mention. | Reply to a message, or mention the participant you mean. |
31
+ | 400 `invalid_status` | `--status` is not one of `open`, `in_progress`, `in_review`, `done`, `cancelled`. | Send one of those five values. |
32
+ | 400 `unknown_property_key` | The property key is not in the registry. | See `task-properties.md` for the registered keys. |
33
+ | 400 `property_value_too_long` | The property value is over 8 KiB. | Store a reference, not a document. |
34
+ | 400 `invalid_json`, `invalid_json_body`, `invalid_message_body`, `deprecated_mentions_not_allowed`, `bad_request`, `missing_turn_execution_id`, `empty_message_body`, `title_required`, `invalid_property_value`, `no_updates`; 403 `browser_origin_not_allowed`; 405 `method_not_allowed` | The request was not the shape, the header set or the invocation the route accepts. | The CLI builds these requests itself and refuses the bad invocations with exit `2` before sending, so reaching one of these means the call was wrapped or rewritten. Run the CLI directly. |
35
+ | 404 `property_not_found` | `task delete-property` for a key the task does not carry. | Read the task's `properties` first. |
36
+ | 413 `request_body_too_large` | An auxiliary send over the 512-byte request cap. | Shorten it. Non-ASCII characters cost several bytes each. |
37
+ | 502 `upstream_error` on a send | Most often a mention of someone who is not a member of this channel: the server refuses the mention, and the gateway has no case for that refusal, so it surfaces as a bare upstream failure. | Drop or correct the mention — `users` spans every channel you belong to, so a participant it lists need not be in this one. Do not repeat the same body. |
38
+ | 502 `upstream_error`, 500 `internal_error` | The gateway reached the server and the call failed there. | On a read, retry once. On a send, take the row above first; a repeat of the same body is not a fix. Report the failure in your answer rather than working around it. |
@@ -0,0 +1,30 @@
1
+ # Task properties
2
+
3
+ A task property associates a task with something that lives outside it. Use one to record what a reader would otherwise have to hunt for in the thread — the pull request that implements the task, the issue it came from.
4
+
5
+ Read them back with `task get`: every task response carries a `properties` object, `{}` when the task has none.
6
+
7
+ ```
8
+ borgee-agent --gateway <path> task set-property <task-id> --key link.pr --value https://github.com/org/repo/pull/12
9
+ borgee-agent --gateway <path> task delete-property <task-id> --key link.pr
10
+ ```
11
+
12
+ The task id is omitted only inside that task's own thread, where the request resolves to the thread's task.
13
+
14
+ ## Registered keys
15
+
16
+ The key set is closed; writing an unregistered key is rejected with `unknown_property_key`.
17
+
18
+ | Key | What it holds |
19
+ | --- | --- |
20
+ | `link.pr` | The pull request that implements this task. Set it as soon as the PR exists, not at the end. |
21
+ | `link.issue` | The issue or ticket the task originates from. |
22
+ | `agent.session_id` | Do not write this. It is registered, so a write is accepted and lands — overwriting the host's record of which provider session worked this task. The host writes it itself after each turn. |
23
+
24
+ ## One key per call
25
+
26
+ Each call writes exactly one key, and that is what makes it safe to write a property while another agent writes a different one on the same task: neither write can clobber the other's key.
27
+
28
+ ## Value
29
+
30
+ A value is a plain string of at most 8 KiB; a longer one is rejected with `property_value_too_long`. It is a reference — a URL, an identifier — never a document.