@ziggs-ai/api-client 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,126 @@
1
+ /**
2
+ * Fields that name a party to reach. A `psn_` persona face is display state
3
+ * and must never be offered here — chat_open / chat_send refuse it, and the
4
+ * baseline's provider learned that only by being refused.
5
+ */
6
+ const ADDRESS_FIELDS = ['participantId', 'to', 'receiverId'];
7
+ /**
8
+ * Required arguments for the capability keys this helper is asked to emit.
9
+ * Kept next to the HTTP bindings so a ready call cannot advertise a schema
10
+ * the definition does not have. Keys we do not list here stay "no extra
11
+ * required args" — the caller already chose what to pre-fill. Tests pin
12
+ * these against live `CapabilityDefinition.params` for keys that have one.
13
+ */
14
+ const REQUIRED_ARGS = {
15
+ chat_open: ['participantId'],
16
+ chat_send: ['chatId'],
17
+ agreement_claim: ['agreementId'],
18
+ agreement_respond: ['agreementId'],
19
+ introduction_redeem: ['token'],
20
+ context_issue_grant: ['holderId', 'scopeKind', 'scopeId'],
21
+ };
22
+ /**
23
+ * Published HTTP invocation per capability key. Paths match the api-client
24
+ * HTTP clients, not a restated OpenAPI list. Path params stay as `:name`
25
+ * until a ready arg fills them — an incomplete call must not invent an id.
26
+ */
27
+ const HTTP_BINDINGS = {
28
+ chat_open: { method: 'POST', path: '/chats' },
29
+ chat_send: { method: 'POST', path: '/chats/:chatId/messages' },
30
+ introduction_list: { method: 'GET', path: '/introductions' },
31
+ introduction_mint: { method: 'POST', path: '/introductions' },
32
+ introduction_redeem: { method: 'POST', path: '/introductions/:token/redeem' },
33
+ agreement_claim: { method: 'POST', path: '/agreements/:agreementId/claim' },
34
+ agreement_respond: {
35
+ method: 'PUT',
36
+ path: '/agreements/:agreementId/approvals/:partyId',
37
+ },
38
+ marketplace_view: { method: 'GET', path: '/marketplace/offers' },
39
+ context_issue_grant: { method: 'POST', path: '/context/grants' },
40
+ open: { method: 'POST', path: '/context/open' },
41
+ access_explain: { method: 'POST', path: '/context/access/explain' },
42
+ inbox_peek: { method: 'GET', path: '/inbox/peek' },
43
+ link_list: { method: 'GET', path: '/agreements?engagementKind=link' },
44
+ link_propose: { method: 'POST', path: '/agreements/links' },
45
+ };
46
+ /**
47
+ * Intentional surface exclusions. A ready next call has to exist as a tool
48
+ * on the calling surface — SDK context exposes delegate, not a top-level
49
+ * issue grant (tool-surface-parity).
50
+ */
51
+ const SURFACE_EXPOSURE = {
52
+ context_issue_grant: ['mcp'],
53
+ };
54
+ const MATERIAL_EFFECTS = {
55
+ agreement_claim: {
56
+ commitment: 'claiming makes you a party to the posted terms',
57
+ relationship: 'you become the open party on that broadcast',
58
+ },
59
+ agreement_respond: {
60
+ commitment: 'approve or reject binds your side; do not sign for a human whose slot this is',
61
+ },
62
+ introduction_redeem: {
63
+ relationship: 'a stranger redeem stages a link both humans must approve — it is not a live connection yet',
64
+ },
65
+ context_issue_grant: {
66
+ disclosure: 'issues a grant that widens who can read that scope',
67
+ },
68
+ link_propose: {
69
+ relationship: 'proposes a reach-only link; nothing is shared until they accept',
70
+ },
71
+ chat_open: {
72
+ disclosure: 'opening a room issues the other side a grant on it',
73
+ },
74
+ };
75
+ /** A persona face (`psn_…`) is display state, not a message address. */
76
+ export function isPersonaFace(id) {
77
+ return typeof id === 'string' && id.startsWith('psn_');
78
+ }
79
+ /** Acting agent and lane from the env. Never a caller-chosen principal. */
80
+ export function workContextFromEnv(env) {
81
+ const ctx = {
82
+ ...(env.creds.agentId ? { actingAgentId: env.creds.agentId } : {}),
83
+ ...(env.creds.laneId ? { laneId: env.creds.laneId } : {}),
84
+ };
85
+ return ctx.actingAgentId || ctx.laneId ? ctx : undefined;
86
+ }
87
+ function isFilled(value) {
88
+ return typeof value === 'string' ? value.trim().length > 0 : value != null;
89
+ }
90
+ function bindHttpPath(template, args) {
91
+ if (!args)
92
+ return template;
93
+ return template.replace(/:([A-Za-z][A-Za-z0-9]*)/g, (all, name) => {
94
+ const value = args[name];
95
+ if (typeof value === 'string' && value && !isPersonaFace(value)) {
96
+ return encodeURIComponent(value);
97
+ }
98
+ return all;
99
+ });
100
+ }
101
+ function toolName(env, capabilityKey) {
102
+ return env.surface === 'mcp' ? `ziggs_${capabilityKey}` : capabilityKey;
103
+ }
104
+ function requiredArgNames(capabilityKey, definition) {
105
+ if (definition) {
106
+ return Object.entries(definition.params)
107
+ .filter(([, param]) => param.required)
108
+ .map(([name]) => name);
109
+ }
110
+ return REQUIRED_ARGS[capabilityKey] ?? [];
111
+ }
112
+ function isKnownCapability(capabilityKey, definition) {
113
+ return (definition != null ||
114
+ capabilityKey in REQUIRED_ARGS ||
115
+ capabilityKey in HTTP_BINDINGS);
116
+ }
117
+ function isExposedOnSurface(capabilityKey, surface) {
118
+ const allowed = SURFACE_EXPOSURE[capabilityKey];
119
+ return allowed ? allowed.includes(surface) : true;
120
+ }
121
+ function effectsFor(capabilityKey, opts) {
122
+ return opts?.effects ?? MATERIAL_EFFECTS[capabilityKey];
123
+ }
1
124
  /**
2
125
  * Build a next step naming the tool as the calling surface registers it.
3
126
  *
@@ -6,13 +129,107 @@
6
129
  * the 31 capability definitions follows that one rule, so the key is enough and
7
130
  * a caller cannot accidentally emit a name the surface does not have.
8
131
  */
9
- export function nextCall(env, capabilityKey, args, why) {
132
+ export function nextCall(env, capabilityKey, args, why, opts) {
133
+ const ctx = workContextFromEnv(env);
134
+ const effects = effectsFor(capabilityKey, opts);
135
+ const extra = {
136
+ ...(effects ? { effects } : {}),
137
+ ...(ctx ? { workContext: ctx } : {}),
138
+ };
139
+ if (!isKnownCapability(capabilityKey, opts?.definition)) {
140
+ return {
141
+ tool: toolName(env, capabilityKey),
142
+ why: `${why} — this capability is not exposed as a next call`,
143
+ ready: false,
144
+ outcome: 'unavailable',
145
+ ...extra,
146
+ };
147
+ }
148
+ if (!isExposedOnSurface(capabilityKey, env.surface)) {
149
+ return {
150
+ tool: toolName(env, capabilityKey),
151
+ why: `${why} — not on this surface`,
152
+ ready: false,
153
+ outcome: 'unavailable',
154
+ ...extra,
155
+ };
156
+ }
157
+ const missing = [];
158
+ const filled = {};
159
+ for (const [key, value] of Object.entries(args ?? {})) {
160
+ if (!isFilled(value))
161
+ continue;
162
+ if (ADDRESS_FIELDS.includes(key) &&
163
+ isPersonaFace(value)) {
164
+ missing.push(key);
165
+ continue;
166
+ }
167
+ filled[key] = value;
168
+ }
169
+ for (const name of requiredArgNames(capabilityKey, opts?.definition)) {
170
+ if (!isFilled(filled[name])) {
171
+ if (!missing.includes(name))
172
+ missing.push(name);
173
+ }
174
+ }
175
+ const hold = opts?.hold;
176
+ const ready = missing.length === 0 && !hold;
177
+ const outcome = opts?.outcome ??
178
+ (hold
179
+ ? 'pending'
180
+ : missing.length === 0
181
+ ? 'ok'
182
+ : Object.keys(filled).length
183
+ ? 'partial'
184
+ : 'unavailable');
185
+ const http = HTTP_BINDINGS[capabilityKey];
186
+ const faceMissing = missing.some((name) => ADDRESS_FIELDS.includes(name));
187
+ const reason = hold && missing.length === 0
188
+ ? why
189
+ : !ready && faceMissing
190
+ ? `${why} — a next call for reaching a person names the user id or their agent, never a psn_ face`
191
+ : !ready
192
+ ? `${why} — missing ${missing.join(', ')}`
193
+ : why;
10
194
  return {
11
- tool: env.surface === 'mcp' ? `ziggs_${capabilityKey}` : capabilityKey,
12
- ...(args && Object.keys(args).length ? { args } : {}),
13
- why,
195
+ tool: toolName(env, capabilityKey),
196
+ ...(Object.keys(filled).length ? { args: filled } : {}),
197
+ why: reason,
198
+ ready,
199
+ outcome,
200
+ ...(missing.length ? { missing } : {}),
201
+ ...(hold ? { hold } : {}),
202
+ ...(http
203
+ ? { http: { method: http.method, path: bindHttpPath(http.path, filled) } }
204
+ : {}),
205
+ ...extra,
14
206
  };
15
207
  }
208
+ /**
209
+ * E23: messages, artifacts, and agreement prose are data. They cannot mint
210
+ * a server action. The only legal parse is "none".
211
+ */
212
+ export function nextCallsFromUntrustedContent(_content) {
213
+ return [];
214
+ }
215
+ /**
216
+ * E22: a next call advertised against an earlier snapshot is stale when the
217
+ * row is no longer in the state that action assumed. Execution still has to
218
+ * hit the existing precondition (claim 400, respond refuse) — this is the
219
+ * client-side invalidation so a caller does not treat the suggestion as live.
220
+ */
221
+ export function isStaleAdvertisedAction(advertised, current) {
222
+ const key = advertised.tool.replace(/^ziggs_/, '');
223
+ const status = current.status ?? '';
224
+ if (key === 'agreement_claim') {
225
+ return status.length > 0 && status !== 'open';
226
+ }
227
+ if (key === 'agreement_respond') {
228
+ return (status.length > 0 &&
229
+ !['open', 'proposed', 'pending'].includes(status));
230
+ }
231
+ return false;
232
+ }
16
233
  /**
17
234
  * The other PRINCIPAL in a two-party agreement, from the perspective of
18
235
  * `selfId`.
@@ -33,6 +33,37 @@ export interface ListArtifactsQuery {
33
33
  * typed now, where it was `unknown[]`.
34
34
  */
35
35
  export type ListArtifactsResult = ListReachableArtifactsResult;
36
+ /**
37
+ * What `GET /artifacts/find` actually looked at.
38
+ *
39
+ * The whole reason the route is separate from the listing: a short answer from
40
+ * a search is ambiguous — nothing matched, or nothing was searched? Everything
41
+ * here exists so a caller can tell those apart without knowing anything about
42
+ * how artifacts are stored or which grants it holds. `completeness: 'complete'`
43
+ * means the query ran over the named arms with none cut short; it never means
44
+ * the artifact is not somewhere else in Ziggs.
45
+ */
46
+ export interface FindArtifactsCoverage {
47
+ method: 'lexical';
48
+ /** The fields the query ran against — not the fields an artifact has. */
49
+ searchedFields: string[];
50
+ /** Reach arms that produced the candidate set, with their sizes. */
51
+ arms: Record<string, number>;
52
+ /** Arms that hit their cap, so older rows in them were never searched. */
53
+ truncatedArms: string[];
54
+ completeness: 'complete' | 'partial' | 'unknown';
55
+ limitations: string[];
56
+ hasMore: boolean;
57
+ nextCursor: string | null;
58
+ }
59
+ export type FindArtifactsResult = ListReachableArtifactsResult & {
60
+ coverage: FindArtifactsCoverage;
61
+ };
62
+ export interface FindArtifactsOptions {
63
+ limit?: number;
64
+ /** Opaque `nextCursor` from a previous page, passed back verbatim. */
65
+ before?: string;
66
+ }
36
67
  export interface WriteArtifactInput {
37
68
  text: string;
38
69
  /** Short list name. An upload defaults to filename when omitted. */
@@ -112,6 +143,22 @@ export declare class ArtifactsClient {
112
143
  */
113
144
  constructor(operatorKey: string, agentId?: string, laneId?: string);
114
145
  list(q: ListArtifactsQuery, opts?: ListArtifactsOptions): Promise<ListArtifactsResult>;
146
+ /**
147
+ * `GET /artifacts/find` — lexical search over the artifacts this agent can
148
+ * already reach: what it authored, what its readable containers hold, and
149
+ * what artifact grants it holds. Bounded by the active lane and shared-room
150
+ * containment, exactly as a point read is.
151
+ *
152
+ * This is not `list()` with a query string. `list()` answers "what is in this
153
+ * container"; a person's cross-container view lives behind `reach` arms that
154
+ * an agent has no business asking for. The server refuses `reach`,
155
+ * `authoredBy` and `chatId` here rather than ignoring them, so a narrowing
156
+ * filter can never be silently dropped — hence no options for them.
157
+ *
158
+ * The result always carries `coverage`. An empty `artifacts` is a statement
159
+ * about what was searched, not about the world.
160
+ */
161
+ find(query: string, opts?: FindArtifactsOptions): Promise<FindArtifactsResult>;
115
162
  /**
116
163
  * Record an agent thought as an `agent-private` artifact. Replaces the
117
164
  * `ContextWriter.recordEvent({ kind: 'thought' })` hack.
@@ -90,6 +90,39 @@ export class ArtifactsClient {
90
90
  }
91
91
  return (await res.json());
92
92
  }
93
+ /**
94
+ * `GET /artifacts/find` — lexical search over the artifacts this agent can
95
+ * already reach: what it authored, what its readable containers hold, and
96
+ * what artifact grants it holds. Bounded by the active lane and shared-room
97
+ * containment, exactly as a point read is.
98
+ *
99
+ * This is not `list()` with a query string. `list()` answers "what is in this
100
+ * container"; a person's cross-container view lives behind `reach` arms that
101
+ * an agent has no business asking for. The server refuses `reach`,
102
+ * `authoredBy` and `chatId` here rather than ignoring them, so a narrowing
103
+ * filter can never be silently dropped — hence no options for them.
104
+ *
105
+ * The result always carries `coverage`. An empty `artifacts` is a statement
106
+ * about what was searched, not about the world.
107
+ */
108
+ async find(query, opts = {}) {
109
+ const q = query?.trim();
110
+ if (!q) {
111
+ throw new Error('ArtifactsClient.find: a query is required — find searches what you can reach, it does not list it');
112
+ }
113
+ const url = new URL(`${getBackendUrl()}/artifacts/find`);
114
+ url.searchParams.set('q', q);
115
+ if (opts.limit != null)
116
+ url.searchParams.set('limit', String(opts.limit));
117
+ if (opts.before)
118
+ url.searchParams.set('before', opts.before);
119
+ const res = await fetch(url.toString(), { headers: this._headers() });
120
+ if (!res.ok) {
121
+ const body = await res.text().catch(() => '');
122
+ throwApiError(res, body, `findArtifacts failed: ${res.status} ${res.statusText}`);
123
+ }
124
+ return (await res.json());
125
+ }
93
126
  /**
94
127
  * Record an agent thought as an `agent-private` artifact. Replaces the
95
128
  * `ContextWriter.recordEvent({ kind: 'thought' })` hack.
@@ -0,0 +1,70 @@
1
+ export declare const CONTEXT_OPEN_KINDS: readonly ["artifact", "chat", "task", "agreement"];
2
+ export type ContextOpenKind = (typeof CONTEXT_OPEN_KINDS)[number];
3
+ export type ContextOpenOutcome = 'ok' | 'unreadable' | 'hidden';
4
+ export interface ContextOpenRef {
5
+ kind: ContextOpenKind;
6
+ id: string;
7
+ }
8
+ export interface ContextOpenBody {
9
+ artifactId?: string;
10
+ chatId?: string;
11
+ taskId?: string;
12
+ agreementId?: string;
13
+ cursor?: string;
14
+ after?: string;
15
+ limit?: number;
16
+ }
17
+ export interface ContextOpenAbilities {
18
+ read: boolean;
19
+ reply: boolean;
20
+ share: boolean;
21
+ delegate: boolean;
22
+ approve: boolean;
23
+ download: boolean;
24
+ }
25
+ export interface ContextOpenAccess {
26
+ abilities: ContextOpenAbilities;
27
+ summary: string;
28
+ document?: {
29
+ empty: boolean;
30
+ extractionStatus?: string | null;
31
+ };
32
+ continuation?: {
33
+ kind: 'owner_decision';
34
+ summary: string;
35
+ };
36
+ }
37
+ export interface ContextOpenResult {
38
+ outcome: ContextOpenOutcome;
39
+ ref: ContextOpenRef;
40
+ access: ContextOpenAccess;
41
+ workContext: {
42
+ laneId: string | null;
43
+ };
44
+ page?: {
45
+ type: string;
46
+ via: {
47
+ kind: string;
48
+ id: string;
49
+ };
50
+ items: unknown[];
51
+ hasMore: boolean;
52
+ nextCursor: string | null;
53
+ latestSequence?: string | null;
54
+ };
55
+ }
56
+ export type ContextExplainResult = Omit<ContextOpenResult, 'page'>;
57
+ /**
58
+ * Open or explain a returned reference. The server resolves the read path;
59
+ * the caller does not send type, via, or a grant id.
60
+ */
61
+ export declare class ContextOpenClient {
62
+ private readonly operatorKey;
63
+ private readonly agentId?;
64
+ private readonly baseUrl?;
65
+ private readonly laneId?;
66
+ constructor(operatorKey: string, agentId?: string | undefined, baseUrl?: string | undefined, laneId?: string | undefined);
67
+ open(body: ContextOpenBody): Promise<ContextOpenResult>;
68
+ explain(body: ContextOpenBody): Promise<ContextExplainResult>;
69
+ private post;
70
+ }
@@ -0,0 +1,52 @@
1
+ import { getBackendUrl } from '../utils/urlUtils.js';
2
+ import { pollSurfaceError } from '../shared/rateLimit.js';
3
+ import { buildOperatorHeaders } from './operatorHeaders.js';
4
+ export const CONTEXT_OPEN_KINDS = [
5
+ 'artifact',
6
+ 'chat',
7
+ 'task',
8
+ 'agreement',
9
+ ];
10
+ /**
11
+ * Open or explain a returned reference. The server resolves the read path;
12
+ * the caller does not send type, via, or a grant id.
13
+ */
14
+ export class ContextOpenClient {
15
+ operatorKey;
16
+ agentId;
17
+ baseUrl;
18
+ laneId;
19
+ constructor(operatorKey, agentId, baseUrl, laneId) {
20
+ this.operatorKey = operatorKey;
21
+ this.agentId = agentId;
22
+ this.baseUrl = baseUrl;
23
+ this.laneId = laneId;
24
+ if (!operatorKey)
25
+ throw new Error('ContextOpenClient: operatorKey is required');
26
+ }
27
+ async open(body) {
28
+ return this.post('/context/open', body);
29
+ }
30
+ async explain(body) {
31
+ return this.post('/context/access/explain', body);
32
+ }
33
+ async post(path, body) {
34
+ const filled = ['artifactId', 'chatId', 'taskId', 'agreementId'].filter((key) => typeof body[key] === 'string' && body[key].trim());
35
+ if (filled.length !== 1) {
36
+ throw new Error('ContextOpenClient: pass exactly one of artifactId, chatId, taskId, agreementId');
37
+ }
38
+ const url = `${this.baseUrl || getBackendUrl()}${path}`;
39
+ const res = await fetch(url, {
40
+ method: 'POST',
41
+ headers: {
42
+ ...buildOperatorHeaders(this.operatorKey, this.agentId, { 'Content-Type': 'application/json' }, this.laneId),
43
+ },
44
+ body: JSON.stringify(body),
45
+ });
46
+ const text = await res.text().catch(() => '');
47
+ if (!res.ok) {
48
+ throw pollSurfaceError(`ContextOpenClient ${path}`, res, text);
49
+ }
50
+ return JSON.parse(text);
51
+ }
52
+ }
@@ -36,6 +36,14 @@ export declare class InboxClient {
36
36
  ack(upTo: string, opts?: {
37
37
  handledResourceIds?: string[];
38
38
  }): Promise<InboxAckResult>;
39
+ /**
40
+ * Ack, and if the row still names a host that has already gone away,
41
+ * read once (that is what takes an expired inbox) and ack again.
42
+ * A second host-conflict is a live competitor and is rethrown unchanged.
43
+ */
44
+ ackOrTakeHost(upTo: string, opts?: {
45
+ handledResourceIds?: string[];
46
+ }): Promise<InboxAckResult>;
39
47
  /**
40
48
  * Drop this process's inbox host lease so another client can take
41
49
  * GET /inbox on its next read.
@@ -1,5 +1,6 @@
1
1
  import { getBackendUrl } from '../utils/urlUtils.js';
2
2
  import { pollSurfaceError } from '../shared/rateLimit.js';
3
+ import { ApiError } from '../types.js';
3
4
  import { INBOX_HOST_CLAIMANT_HEADER, hostIdentity, } from '../instanceIdentity.js';
4
5
  /**
5
6
  * The doorbell, not the door: references addressed to this agent
@@ -96,6 +97,23 @@ export class InboxClient {
96
97
  }
97
98
  return JSON.parse(body);
98
99
  }
100
+ /**
101
+ * Ack, and if the row still names a host that has already gone away,
102
+ * read once (that is what takes an expired inbox) and ack again.
103
+ * A second host-conflict is a live competitor and is rethrown unchanged.
104
+ */
105
+ async ackOrTakeHost(upTo, opts = {}) {
106
+ try {
107
+ return await this.ack(upTo, opts);
108
+ }
109
+ catch (err) {
110
+ if (!(err instanceof ApiError) || err.code !== 'INBOX_HOST_CONFLICT') {
111
+ throw err;
112
+ }
113
+ await this.getInbox();
114
+ return await this.ack(upTo, opts);
115
+ }
116
+ }
99
117
  /**
100
118
  * Drop this process's inbox host lease so another client can take
101
119
  * GET /inbox on its next read.
@@ -6,6 +6,8 @@ import { type Creds, type Task, type TaskState } from '../types.js';
6
6
  export interface TaskWriteConfirm {
7
7
  ok: true;
8
8
  taskId: string;
9
+ /** The agreement the task hangs under, when the server reports it. */
10
+ agreementId?: string;
9
11
  state: TaskState;
10
12
  updatedAt?: string;
11
13
  status?: string;
@@ -37,6 +37,7 @@ function extractWriteConfirm(data) {
37
37
  return {
38
38
  ok: true,
39
39
  taskId: d['taskId'],
40
+ ...(typeof d['agreementId'] === 'string' ? { agreementId: d['agreementId'] } : {}),
40
41
  state: d['state'],
41
42
  ...(typeof d['updatedAt'] === 'string' || d['updatedAt'] instanceof Date
42
43
  ? { updatedAt: String(d['updatedAt']) }
@@ -9,6 +9,8 @@ export { ArtifactsClient, artifactScopeForSession, AGREEMENT_LANE_PREFIX, ARTIFA
9
9
  export type { ArtifactVisibility, ListArtifactsOptions, ListArtifactsQuery, ListArtifactsResult, WriteArtifactInput, } from './ArtifactsClient.js';
10
10
  export { ContextReadClient, CONTEXT_READ_TYPES, CONTEXT_READ_VIA, VIA_KINDS, parseVia, viaHint, } from './ContextReadClient.js';
11
11
  export type { ContextReadType, ContextReadQuery, ContextReadEnvelope, ContextSnapshotResult, ContextSnapshotParticipant, ViaKind, } from './ContextReadClient.js';
12
+ export { ContextOpenClient, CONTEXT_OPEN_KINDS } from './ContextOpenClient.js';
13
+ export type { ContextOpenKind, ContextOpenOutcome, ContextOpenRef, ContextOpenBody, ContextOpenAbilities, ContextOpenAccess, ContextOpenResult, ContextExplainResult, } from './ContextOpenClient.js';
12
14
  export { ContextDiscoveryClient } from './ContextDiscoveryClient.js';
13
15
  export type { DiscoverableItem } from './ContextDiscoveryClient.js';
14
16
  export { IntroductionsClient } from './IntroductionsClient.js';
@@ -8,6 +8,7 @@ export { ArtifactsClient,
8
8
  // agreement lanes are not chats — callers scope artifact writes with this.
9
9
  artifactScopeForSession, AGREEMENT_LANE_PREFIX, ARTIFACT_INLINE_TEXT_MAX_CHARS, ARTIFACT_INLINE_TEXT_OVER_LIMIT_HINT, } from './ArtifactsClient.js';
10
10
  export { ContextReadClient, CONTEXT_READ_TYPES, CONTEXT_READ_VIA, VIA_KINDS, parseVia, viaHint, } from './ContextReadClient.js';
11
+ export { ContextOpenClient, CONTEXT_OPEN_KINDS } from './ContextOpenClient.js';
11
12
  export { ContextDiscoveryClient } from './ContextDiscoveryClient.js';
12
13
  export { IntroductionsClient } from './IntroductionsClient.js';
13
14
  export { GrantsClient } from './GrantsClient.js';
package/dist/index.d.ts CHANGED
@@ -18,3 +18,5 @@ export { agreementLaneId, agreementIdFromLane, isAgreementLaneId, isAgreementId
18
18
  export * from './shared/operatorKey.js';
19
19
  export { INBOX_HOST_CLAIMANT_HEADER, INSTANCE_IDENTITY_MAX_LENGTH, cliHostIdentity, hostIdentity, instanceIdentity, mcpConnectionIdentity, } from './instanceIdentity.js';
20
20
  export * from './utils/appUrls.js';
21
+ export { sessionOrientation, } from './sessionOrientation.js';
22
+ export type { ContinuationKind, InboxAckDriver, InboxHandling, RepresentedPerson, SessionContinuation, SessionOrientation, SessionOrientationInput, } from './sessionOrientation.js';
package/dist/index.js CHANGED
@@ -18,3 +18,4 @@ export { agreementLaneId, agreementIdFromLane, isAgreementLaneId, isAgreementId
18
18
  export * from './shared/operatorKey.js';
19
19
  export { INBOX_HOST_CLAIMANT_HEADER, INSTANCE_IDENTITY_MAX_LENGTH, cliHostIdentity, hostIdentity, instanceIdentity, mcpConnectionIdentity, } from './instanceIdentity.js';
20
20
  export * from './utils/appUrls.js';
21
+ export { sessionOrientation, } from './sessionOrientation.js';
@@ -25,7 +25,7 @@ export declare function cliHostIdentity(): string;
25
25
  *
26
26
  * Not the backend task's identity. That process serves every connected
27
27
  * assistant at once, so its own identity makes them one host and changes under
28
- * all of them on every deploy — a hosted read answering 409 for up to 330
28
+ * all of them on every deploy — a hosted read answering 409 for up to 220
29
29
  * seconds after each restart, and replicas refusing each other's agents.
30
30
  *
31
31
  * The key survives both. A token refresh re-signs the same `keyId`, so an
@@ -4,7 +4,7 @@ import { decodeOperatorKeyClaims } from './shared/operatorKey.js';
4
4
  *
5
5
  * `X-Ziggs-Instance` decides inbox ownership: the first host to read acquires
6
6
  * an agent's whole inbox, a second host is refused with
7
- * `409 INBOX_HOST_CONFLICT` until the owner has stopped renewing for 330
7
+ * `409 INBOX_HOST_CONFLICT` until the owner has stopped renewing for 220
8
8
  * seconds, and an ack renews an owner but never acquires. A missing or
9
9
  * overlong value is `400 INBOX_HOST_REQUIRED`.
10
10
  *
@@ -71,7 +71,7 @@ export function cliHostIdentity() {
71
71
  *
72
72
  * Not the backend task's identity. That process serves every connected
73
73
  * assistant at once, so its own identity makes them one host and changes under
74
- * all of them on every deploy — a hosted read answering 409 for up to 330
74
+ * all of them on every deploy — a hosted read answering 409 for up to 220
75
75
  * seconds after each restart, and replicas refusing each other's agents.
76
76
  *
77
77
  * The key survives both. A token refresh re-signs the same `keyId`, so an
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Who this session is, who it represents, and whether it can continue
3
+ * unattended. Built from the binding the caller already has. Does not
4
+ * read mail and does not take the inbox lease.
5
+ */
6
+ export type InboxAckDriver = 'agent' | 'host';
7
+ export type ContinuationKind = 'manual' | 'scheduled';
8
+ export interface RepresentedPerson {
9
+ /** The human this agent acts for. Null when the key does not name one. */
10
+ id: string | null;
11
+ kind: 'user';
12
+ }
13
+ export interface SessionContinuation {
14
+ kind: ContinuationKind;
15
+ canScheduleWake: boolean;
16
+ summary: string;
17
+ }
18
+ export interface InboxHandling {
19
+ /** Who closes the inbox loop. Never both on the same identity. */
20
+ ackDriver: InboxAckDriver;
21
+ readAcquiresHost: true;
22
+ peekDoesNotAcquire: true;
23
+ peek: {
24
+ tool: string;
25
+ http: {
26
+ method: 'GET';
27
+ path: '/inbox/peek';
28
+ };
29
+ };
30
+ }
31
+ export interface SessionOrientation {
32
+ representedPerson: RepresentedPerson;
33
+ continuation: SessionContinuation;
34
+ inboxHandling: InboxHandling;
35
+ identity: {
36
+ agentId: string;
37
+ distinctFromBackgroundWorker: true;
38
+ };
39
+ }
40
+ export interface SessionOrientationInput {
41
+ agentId: string;
42
+ ownerUserId: string | null;
43
+ /** MCP assistants ack themselves. The hosted SDK host acks; the brain does not. */
44
+ surface: 'mcp' | 'sdk';
45
+ }
46
+ /**
47
+ * Facts a cold entry needs that `session.ownerId` never labelled: the
48
+ * represented person, whether this host can arm a wake, and who drives ack.
49
+ */
50
+ export declare function sessionOrientation(input: SessionOrientationInput): SessionOrientation;