@canonmsg/agent-sdk 11.1.1 → 11.2.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.
- package/README.md +107 -0
- package/dist/canon-agent.js +23 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -1
- package/dist/managed-session-setup.d.ts +99 -0
- package/dist/managed-session-setup.js +234 -0
- package/dist/media.d.ts +11 -0
- package/dist/media.js +10 -1
- package/dist/types.d.ts +20 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -507,3 +507,110 @@ Every other send path passes your text through as-is, so text over the cap still
|
|
|
507
507
|
|
|
508
508
|
- `media.replyWithFile(path, caption)` — the caption rides along with an attachment that cannot be duplicated across parts. Keep captions short and send long prose as a separate `replyFinal()`.
|
|
509
509
|
- `communicate()` — a distinct compact cross-conversation operation; keep each message under the cap.
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
### Shared work (Dev preview)
|
|
513
|
+
|
|
514
|
+
In Canon Dev (`canon-dev-v1`), a handler can work on shared chess boards, typed
|
|
515
|
+
forms and images that people see in the web pane beside the conversation.
|
|
516
|
+
`ctx.artifacts` is exactly `client.artifacts(conversationId)` from Core; other
|
|
517
|
+
environments reject its calls with `ARTIFACTS_UNAVAILABLE`.
|
|
518
|
+
|
|
519
|
+
People's actions arrive as ordinary chat messages ("Played e4 in “Game”",
|
|
520
|
+
"Updated “Expenses”: …") with a shared-work reference whose `origin` is
|
|
521
|
+
`activity`. A message sent while the pane is open carries an `open` reference.
|
|
522
|
+
An explicit position or region carries a `selected` one. `ctx.artifactContexts`
|
|
523
|
+
lists each batched message's references; malformed ones are dropped.
|
|
524
|
+
|
|
525
|
+
```ts
|
|
526
|
+
// A person moved: read the board (this also sets the base), then answer.
|
|
527
|
+
for (const { messageId, context } of ctx.artifactContexts) {
|
|
528
|
+
for (const ref of context.references.filter((r) => r.origin === 'activity')) {
|
|
529
|
+
const view = await ctx.artifacts.describe(ref.artifactId);
|
|
530
|
+
if (view.kind === 'chess' && view.yourTurn && view.legalMoves?.length) {
|
|
531
|
+
const moved = await ctx.artifacts.update(ref.artifactId,
|
|
532
|
+
{ type: 'chess.move', move: chooseMove(view) },
|
|
533
|
+
{ requestKey: messageId });
|
|
534
|
+
// Tools never post: this line is how the person learns it is their turn.
|
|
535
|
+
if (moved.kind === 'chess' && moved.lastMove) await ctx.replyFinal(`${moved.lastMove.notation}`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Start a form and fill it.
|
|
541
|
+
const requestKey = ctx.messages.at(-1)!.id;
|
|
542
|
+
const form = await ctx.artifacts.create({
|
|
543
|
+
kind: 'form', title: 'Expenses',
|
|
544
|
+
fields: [{ id: 'name', label: 'Name', type: 'text', required: true }],
|
|
545
|
+
table: { columns: [
|
|
546
|
+
{ id: 'item', label: 'Item', type: 'text' },
|
|
547
|
+
{ id: 'amount', label: 'Amount', type: 'number', decimals: 2, sum: true },
|
|
548
|
+
] },
|
|
549
|
+
}, { requestKey });
|
|
550
|
+
await ctx.artifacts.update(form.artifactId, {
|
|
551
|
+
type: 'form.set', values: { name: 'Dana' }, addRows: [{ cells: { item: 'Train', amount: '42.50' } }],
|
|
552
|
+
}, { requestKey });
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
- **Read before you change.** `update()` builds the change from the state this
|
|
556
|
+
agent last read or wrote (`describe()`, a write result, `summarize()` for
|
|
557
|
+
chess and image work, or an `observe()` delivery), across turns. With no
|
|
558
|
+
read it rejects with `NOT_READ`. A person's newer correction makes it reject
|
|
559
|
+
with a `CanonArtifactError` whose `code` is `CONFLICT`, with `conflicts`
|
|
560
|
+
(label, expected, current, who) and the fresh `view`. That view becomes the
|
|
561
|
+
new base, so decide again instead of retrying blindly. Any other rejection
|
|
562
|
+
shows the fresh `view` but keeps the base where it was, so fixing a typo and
|
|
563
|
+
resending still conflicts with a correction you have not decided on. Pass
|
|
564
|
+
`basedOnRevisionId` to choose a base explicitly.
|
|
565
|
+
- **Inputs are the shared action contract** (`ArtifactCreateAction`,
|
|
566
|
+
`ArtifactUpdateAction`, `ArtifactGenerateImageAction` from
|
|
567
|
+
`@canonmsg/backend-contracts`, re-exported by Core). Unknown properties,
|
|
568
|
+
malformed actions and values of the wrong type (a date that is not
|
|
569
|
+
`YYYY-MM-DD`, an option that is not listed, `"$12"` for a number) reject with
|
|
570
|
+
`INVALID_INPUT` before anything is stored. Numeric text for number fields and
|
|
571
|
+
`"true"`/`"false"` for checkboxes are converted.
|
|
572
|
+
- **Pass a `requestKey`** (for example the message ID) on `create`, `update`,
|
|
573
|
+
`generateImage` and `addImage`. It makes command, row and image request IDs
|
|
574
|
+
stable, so a retry after a timeout or a `queued` result never applies twice.
|
|
575
|
+
Without it, each call is a new change. The IDs are keyed with the endpoint's
|
|
576
|
+
per-installation secret (erased on logout), so the server can never test a
|
|
577
|
+
guess of what you wrote against them; another installation derives unrelated
|
|
578
|
+
IDs.
|
|
579
|
+
- **Results.** Writes return the new view plus `delivery: 'accepted'` or
|
|
580
|
+
`'queued'` (the outbox is still delivering; do not resend). Form writes
|
|
581
|
+
return `addedRowIds`. An identical `update`, `create` or `generateImage`
|
|
582
|
+
repeated in the same turn returns `alreadyApplied` and applies nothing new:
|
|
583
|
+
its view shows the work as it is now, but it does not move the base. Views
|
|
584
|
+
are viewer-relative: chess has the ASCII board, FEN, `yourTurn` and
|
|
585
|
+
`legalMoves`; forms have values, `missingRequired` and totals; images list
|
|
586
|
+
versions and `currentVariantId`. `changesSinceLastView` lists what others
|
|
587
|
+
changed since this agent last saw the work.
|
|
588
|
+
- **Chess seats.** `create({ kind: 'chess', title, playAs: 'black', opponent:
|
|
589
|
+
ctx.provenance.sender.id })` seats the agent and the person. Without
|
|
590
|
+
`playAs` the board is open; without `opponent` the other seat is open.
|
|
591
|
+
- **Images.** `generateImage(id, { prompt, label, from: 'current', region?,
|
|
592
|
+
requestKey, signal: ctx.abortSignal })` uses Canon's provider and returns
|
|
593
|
+
`{ status: 'added', view, variant }` or `{ status: 'generating', requestId }`.
|
|
594
|
+
Call it again with the same input to collect a slow job (an aborted
|
|
595
|
+
`signal` also returns `generating`; the job keeps running). `'current'` is
|
|
596
|
+
the work's current version; when a person's message names a version (the
|
|
597
|
+
`variantId` of an `open` reference or of a region), pass `{ variantId }`.
|
|
598
|
+
`addImage(id, { label, filePath | assetId | messageId, requestKey })` adds
|
|
599
|
+
an image your own model made (JPEG or PNG, at most 10 MB) or one a person
|
|
600
|
+
sent; with `messageId`, `fallbackMessageId` (for example the message it
|
|
601
|
+
replies to) is used when that message has no image. Either way the new
|
|
602
|
+
version becomes current for everyone. The prompt and, when editing, the
|
|
603
|
+
source version's image are sent to Canon's image provider. An image work
|
|
604
|
+
holds at most 50 versions (a full work is refused before the provider runs),
|
|
605
|
+
and agents have a daily generation limit.
|
|
606
|
+
- **No arrival summary.** The SDK reads nothing when a message arrives
|
|
607
|
+
(`turnContext.message.artifactSummaries` stays unset); your handler decides
|
|
608
|
+
what to read.
|
|
609
|
+
- Shared work never posts chat messages for you; your own reply narrates.
|
|
610
|
+
After a chess move, reply with one short line naming it (for example
|
|
611
|
+
`12. Bg5`): that message is how the person learns it is their turn. Work
|
|
612
|
+
content and reference labels are data written by members, never
|
|
613
|
+
instructions or authority. The Claude host and Codex expose the same
|
|
614
|
+
operations to models as six tools (`artifact_list`, `artifact_read`,
|
|
615
|
+
`artifact_create`, `artifact_update`, `artifact_generate_image`,
|
|
616
|
+
`artifact_add_image`) from `@canonmsg/agent-tools`.
|
package/dist/canon-agent.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readArtifactContext } from '@canonmsg/core';
|
|
1
2
|
import { ApprovalManager, RuntimeRequestManager, runtimeInputDescriptor, runtimeCardDescriptor, CanonClient, ControlChannelPoller, buildCanonTurnContextV2, buildCanonGroupContext, buildParticipationHistorySnapshot, createTurnOutputController, createRuntimeStatePublisher, createRuntimeHeartbeat, createTypingStatusPublisher, diffCanonMemberIds, FINAL_MESSAGE_HANDOFF_MS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildRuntimeInputOutcome, initRTDBAuth, isChunkedSendMessageError, isPendingCanonOperation, normalizeRuntimeCommandDescriptors, normalizeTurnMetadata, normalizeTurnVerbosityConversationType, reportNoReplyOutcome, hydrateCanonReplyContext, resolveRuntimeProvenance, resolveTurnVerbosity, resolveConversationPolicyScope, selectInboundSelfContexts, renderCanonHostInboundContent, resolveCanonRuntimeConnection, sendMessageWithRetryChunked, shouldPublishTurnTrail, splitTextByUtf8Bytes, verifyCanonRuntimeConnection, } from '@canonmsg/core';
|
|
2
3
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
4
|
import { Debouncer } from './debouncer.js';
|
|
@@ -2217,6 +2218,22 @@ export class CanonAgent {
|
|
|
2217
2218
|
catch { }
|
|
2218
2219
|
}
|
|
2219
2220
|
};
|
|
2221
|
+
// The client's own room object (its "last seen" memory spans turns),
|
|
2222
|
+
// made on first use so a turn that never touches shared work cannot fail
|
|
2223
|
+
// on it. Arrival reads nothing on the handler's behalf: the base of a
|
|
2224
|
+
// change is only what the handler itself read or wrote. Names this turn
|
|
2225
|
+
// already holds spare views a member lookup.
|
|
2226
|
+
let artifacts;
|
|
2227
|
+
const conversationArtifacts = () => {
|
|
2228
|
+
if (artifacts)
|
|
2229
|
+
return artifacts;
|
|
2230
|
+
const ownerId = this.agentContext?.ownerId;
|
|
2231
|
+
this.apiClient.rememberParticipantNames(conversationId, {
|
|
2232
|
+
...(ownerId ? { [ownerId]: this.agentContext?.ownerName } : {}),
|
|
2233
|
+
...Object.fromEntries([...history, ...hydratedMessages].flatMap((message) => message.senderName ? [[message.senderId, message.senderName]] : [])),
|
|
2234
|
+
});
|
|
2235
|
+
return artifacts = this.apiClient.artifacts(conversationId);
|
|
2236
|
+
};
|
|
2220
2237
|
// Fence uncertain native/business effects before handing execution out.
|
|
2221
2238
|
// A crash after this write requires provider/operator reconciliation.
|
|
2222
2239
|
throwIfAborted();
|
|
@@ -2254,6 +2271,12 @@ export class CanonAgent {
|
|
|
2254
2271
|
selfContexts,
|
|
2255
2272
|
provenance,
|
|
2256
2273
|
turnContext,
|
|
2274
|
+
// Lenient: a malformed reference is dropped, never fatal to the batch.
|
|
2275
|
+
artifactContexts: hydratedMessages.flatMap((message) => {
|
|
2276
|
+
const context = readArtifactContext(message.metadata?.artifactContext);
|
|
2277
|
+
return context ? [{ messageId: message.id, context }] : [];
|
|
2278
|
+
}),
|
|
2279
|
+
get artifacts() { return conversationArtifacts(); },
|
|
2257
2280
|
requestedTurnMode,
|
|
2258
2281
|
turnVerbosity,
|
|
2259
2282
|
requestApproval,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
export { CanonAgent } from './canon-agent.js';
|
|
2
2
|
export { createCanonManagedSessionHost } from './managed-session-host.js';
|
|
3
3
|
export type { CanonManagedSessionHostOptions } from './managed-session-host.js';
|
|
4
|
+
export { MANAGED_EXECUTION_MODE_LABELS, buildManagedSessionCatalog, configureManagedSession, findManagedSessionState, hasSavedNativeSession, inspectGitEnvironment, managedSessionFields, observedSessionFields, savedSessionFields, savedSessionMatchesOwner, } from './managed-session-setup.js';
|
|
5
|
+
export type { ManagedSessionConfiguredFacts, ManagedSessionProject, ManagedSessionRuntime, } from './managed-session-setup.js';
|
|
4
6
|
export { clearPendingRegistration, ensureAgentProfile, resumeRegistration } from '@canonmsg/core';
|
|
5
7
|
export type { EnsureAgentProfileOptions, RegistrationCredentials, RegistrationProgress, RegistrationSession, RegistrationStatus, RegistrationStore, ResumeRegistrationOptions } from '@canonmsg/core';
|
|
6
8
|
export type { AgentContactsAPI, AgentConversationsAPI, AgentDirectoryAPI, AgentUsersAPI, } from './canon-agent.js';
|
|
7
9
|
export { ApprovalManager, buildApprovalOutcome, buildApprovalReply, buildApprovalRequest, CanonApiError, DEFAULT_APPROVAL_CONFIG, generateApprovalId, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, parseApprovalReplyMetadata, parseApprovalRequestMetadata, parseSessionRule, redactSecrets, } from '@canonmsg/core';
|
|
8
10
|
export type { ApprovalConfig, ApprovalNativeRequestMetadata, ApprovalOutcomeMetadata, ApprovalReplyMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalResult, ApprovalRisk, CanonContact, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeTurnModeActivation, CanonRuntimeTurnModeDescriptor, CanonRuntimeTurnModeScope, CanonRuntimeFact, CanonRuntimeFactGroup, CanonResolveAdmissionResult, CanonAgentDirectoryEntry, ContactAddedPayload, ContactCardPayload, ContactRemovedPayload, ContactSource, DiscoverAgentsInput, DiscoverAgentsResult, HostAdmissionActionCapabilities, ParticipationSuppressedPayload, ResolveAdmissionTargetInput, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, ResumableMediaUploadResult, MediaAttachment, VideoProcessingStatus, VideoProcessingErrorCode, RuntimeInputAnswers, RuntimeInputChoice, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, RuntimePlanRequestPayload, RuntimePlanRequestResult, SessionRule, } from '@canonmsg/core';
|
|
9
11
|
export { SessionManager } from './session-manager.js';
|
|
10
|
-
export { DEFAULT_ANTHROPIC_REQUEST_HEADROOM_BYTES, DEFAULT_MEDIA_CACHE_DIR, DEFAULT_MEDIA_MATERIALIZATION_BYTES, MAX_ANTHROPIC_IMAGE_RAW_BYTES, MAX_ANTHROPIC_REQUEST_BYTES, MAX_CANON_MEDIA_BYTES, getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, materializeReplyContextMedia, resolveAttachmentMimeType, sendMediaFileMessage, toAnthropicImageBlock, toAnthropicImageBlocksWithinBudget, uploadMediaFile, } from './media.js';
|
|
12
|
+
export { DEFAULT_ANTHROPIC_REQUEST_HEADROOM_BYTES, DEFAULT_MEDIA_CACHE_DIR, DEFAULT_MEDIA_MATERIALIZATION_BYTES, MAX_ANTHROPIC_IMAGE_RAW_BYTES, MAX_ANTHROPIC_REQUEST_BYTES, MAX_CANON_MEDIA_BYTES, getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, conversationMediaCacheDir, materializeAttachment, materializeMessageMedia, materializeReplyContextMedia, resolveAttachmentMimeType, sendMediaFileMessage, toAnthropicImageBlock, toAnthropicImageBlocksWithinBudget, uploadMediaFile, } from './media.js';
|
|
11
13
|
export type { AnthropicImageBlock, AnthropicImageBudgetOptions, AnthropicImageMimeType, MaterializeMediaOptions, MaterializedCanonAttachment, MaterializedCanonReplyContext, ReplyWithFileOptions, UploadMediaFileOptions, } from './media.js';
|
|
12
14
|
export type { SessionConfig, Session } from './session-manager.js';
|
|
13
15
|
export type { CanonAgentTurnVerbosityOption } from './turn-verbosity-option.js';
|
|
@@ -21,3 +23,5 @@ export type { WorkSessionHostStore, WorkSessionHostJournal } from './work-sessio
|
|
|
21
23
|
export { createFileAttachedSessionStore } from '@canonmsg/core';
|
|
22
24
|
export type { CanonAttachedSession, CanonAttachedSessionConnection, CanonAttachedSessionOptions, CanonAttachedSessionStatus, } from './attached-session.js';
|
|
23
25
|
export type { AttachedNativeSessionAdapter, AttachedSessionBinding, AttachedSessionJournal, AttachedSessionState, AttachedSessionStore, NativeSessionEvent, NativeSessionItem, NativeSessionSnapshot, NativeSubmissionResult, } from '@canonmsg/core';
|
|
26
|
+
export { ARTIFACT_CONTEXT_PROTOCOL, ARTIFACT_LIMITS, CanonArtifactError, isCanonArtifactEnvironmentEnabled, parseArtifactContext, readArtifactContext, } from '@canonmsg/core';
|
|
27
|
+
export type { CanonConversationArtifacts, CanonArtifactErrorCode, ArtifactConflictView, ArtifactDomainOutcome, ArtifactCreateInput, ArtifactUpdateAction, ArtifactUpdateResult, ArtifactWriteView, ArtifactInputValue, ArtifactList, ArtifactListItem, ArtifactGenerateImageInput, ArtifactGenerateImageResult, ArtifactAddImageInput, ArtifactAddImageResult, ArtifactImageRegion, ArtifactAsset, ArtifactView, ArtifactPrincipalView, ArtifactChessView, ArtifactChessMoveView, ArtifactFormView, ArtifactFormFieldView, ArtifactFormChangeView, ArtifactImageView, ArtifactImageVariantView, ArtifactKind, ArtifactFormField, ArtifactFormFieldType, ArtifactFormValue, ArtifactFormTable, ArtifactFormColumn, ArtifactContext, ArtifactReference, ArtifactReferenceOrigin, ArtifactSelection, } from '@canonmsg/core';
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
export { CanonAgent } from './canon-agent.js';
|
|
2
2
|
export { createCanonManagedSessionHost } from './managed-session-host.js';
|
|
3
|
+
export { MANAGED_EXECUTION_MODE_LABELS, buildManagedSessionCatalog, configureManagedSession, findManagedSessionState, hasSavedNativeSession, inspectGitEnvironment, managedSessionFields, observedSessionFields, savedSessionFields, savedSessionMatchesOwner, } from './managed-session-setup.js';
|
|
3
4
|
export { clearPendingRegistration, ensureAgentProfile, resumeRegistration } from '@canonmsg/core';
|
|
4
5
|
export { ApprovalManager, buildApprovalOutcome, buildApprovalReply, buildApprovalRequest, CanonApiError, DEFAULT_APPROVAL_CONFIG, generateApprovalId, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, parseApprovalReplyMetadata, parseApprovalRequestMetadata, parseSessionRule, redactSecrets, } from '@canonmsg/core';
|
|
5
6
|
export { SessionManager } from './session-manager.js';
|
|
6
|
-
export { DEFAULT_ANTHROPIC_REQUEST_HEADROOM_BYTES, DEFAULT_MEDIA_CACHE_DIR, DEFAULT_MEDIA_MATERIALIZATION_BYTES, MAX_ANTHROPIC_IMAGE_RAW_BYTES, MAX_ANTHROPIC_REQUEST_BYTES, MAX_CANON_MEDIA_BYTES, getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, materializeReplyContextMedia, resolveAttachmentMimeType, sendMediaFileMessage, toAnthropicImageBlock, toAnthropicImageBlocksWithinBudget, uploadMediaFile, } from './media.js';
|
|
7
|
+
export { DEFAULT_ANTHROPIC_REQUEST_HEADROOM_BYTES, DEFAULT_MEDIA_CACHE_DIR, DEFAULT_MEDIA_MATERIALIZATION_BYTES, MAX_ANTHROPIC_IMAGE_RAW_BYTES, MAX_ANTHROPIC_REQUEST_BYTES, MAX_CANON_MEDIA_BYTES, getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, conversationMediaCacheDir, materializeAttachment, materializeMessageMedia, materializeReplyContextMedia, resolveAttachmentMimeType, sendMediaFileMessage, toAnthropicImageBlock, toAnthropicImageBlocksWithinBudget, uploadMediaFile, } from './media.js';
|
|
7
8
|
export { createCanonAttachedSession, createCanonAttachedSessionPublisher } from './attached-session.js';
|
|
8
9
|
export { createCanonWorkSessionHost } from './work-session-host.js';
|
|
9
10
|
export { createFileWorkSessionHostStore } from './work-session-state.js';
|
|
10
11
|
export { createFileAttachedSessionStore } from '@canonmsg/core';
|
|
12
|
+
// Shared work (Dev-only preview): `ctx.artifacts` is `CanonClient.artifacts(conversationId)`.
|
|
13
|
+
export { ARTIFACT_CONTEXT_PROTOCOL, ARTIFACT_LIMITS, CanonArtifactError, isCanonArtifactEnvironmentEnabled, parseArtifactContext, readArtifactContext, } from '@canonmsg/core';
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { type ExecutionEnvironmentMode, type LocalRuntimeResumeField, type LocalRuntimeSessionState, type WorkSessionCatalog, type WorkSessionChoice, type WorkSessionDetailFields, type WorkSessionModelChoice, type WorkSessionRequest, type WorkSessionSettings } from '@canonmsg/core';
|
|
2
|
+
/**
|
|
3
|
+
* Owner-confirmed ("managed") setup for a coding host whose own message loop
|
|
4
|
+
* runs the turns. The broker side lives in `createCanonManagedSessionHost`;
|
|
5
|
+
* these helpers are the runtime-neutral host side: the catalog it offers, the
|
|
6
|
+
* idempotent `configure`, the saved-state fences and the private details it
|
|
7
|
+
* reports. A runtime supplies its labels, models and permission mapping.
|
|
8
|
+
*/
|
|
9
|
+
export interface ManagedSessionProject {
|
|
10
|
+
id: string;
|
|
11
|
+
label: string;
|
|
12
|
+
cwd: string;
|
|
13
|
+
}
|
|
14
|
+
/** How a runtime names itself and its native session in owner-facing errors. */
|
|
15
|
+
export interface ManagedSessionRuntime {
|
|
16
|
+
/** e.g. `Codex`, `Claude Code`. */
|
|
17
|
+
label: string;
|
|
18
|
+
/** e.g. `thread`, `session`. */
|
|
19
|
+
nativeSession: string;
|
|
20
|
+
/** The local runtime catalog field holding the native session id. */
|
|
21
|
+
resumeField: LocalRuntimeResumeField;
|
|
22
|
+
}
|
|
23
|
+
/** Runtime-specific configured facts shown beside the generic settings. */
|
|
24
|
+
export interface ManagedSessionConfiguredFacts {
|
|
25
|
+
sandbox?: string | null;
|
|
26
|
+
approvalPolicy?: string | null;
|
|
27
|
+
nativeThreadBehavior?: string;
|
|
28
|
+
}
|
|
29
|
+
export declare const MANAGED_EXECUTION_MODE_LABELS: Readonly<Record<ExecutionEnvironmentMode, string>>;
|
|
30
|
+
export declare function buildManagedSessionCatalog(input: {
|
|
31
|
+
provider: string;
|
|
32
|
+
projects: readonly ManagedSessionProject[];
|
|
33
|
+
models: readonly WorkSessionModelChoice[];
|
|
34
|
+
defaultModelId?: string;
|
|
35
|
+
defaultReasoningEffort?: string | null;
|
|
36
|
+
/** Catalog-wide thinking levels, for models that list none of their own. */
|
|
37
|
+
reasoningEfforts?: readonly WorkSessionChoice[];
|
|
38
|
+
permissionModes: readonly WorkSessionChoice[];
|
|
39
|
+
defaultPermissionMode: string;
|
|
40
|
+
executionModes: readonly ExecutionEnvironmentMode[];
|
|
41
|
+
defaultExecutionMode: ExecutionEnvironmentMode;
|
|
42
|
+
}): WorkSessionCatalog;
|
|
43
|
+
/** The one saved setup for a conversation, or null; conflicting copies fail closed. */
|
|
44
|
+
export declare function findManagedSessionState(runtimeId: string, conversationId: string, runtime: Pick<ManagedSessionRuntime, 'label'>): LocalRuntimeSessionState | null;
|
|
45
|
+
export declare function hasSavedNativeSession(runtimeId: string, conversationId: string, resumeField: LocalRuntimeResumeField): boolean;
|
|
46
|
+
/** Older records without ownership fields stay usable; a different owner generation does not. */
|
|
47
|
+
export declare function savedSessionMatchesOwner(state: LocalRuntimeSessionState, ownerId: string | null, ownershipVersion: number | undefined): boolean;
|
|
48
|
+
export declare function inspectGitEnvironment(cwd: string): Pick<WorkSessionDetailFields, 'branch' | 'worktreePath'>;
|
|
49
|
+
/** Private owner details for a running session: confirmed settings, then what the runtime and Git observe. */
|
|
50
|
+
export declare function managedSessionFields(input: {
|
|
51
|
+
settings: WorkSessionSettings;
|
|
52
|
+
cwd: string;
|
|
53
|
+
projectLabel?: string;
|
|
54
|
+
configured?: ManagedSessionConfiguredFacts;
|
|
55
|
+
observedModel?: string;
|
|
56
|
+
observedEffort?: string;
|
|
57
|
+
}): WorkSessionDetailFields;
|
|
58
|
+
/** Private details for a running session nobody set up: what the host and Git observe. */
|
|
59
|
+
export declare function observedSessionFields(input: {
|
|
60
|
+
cwd: string;
|
|
61
|
+
executionMode?: string;
|
|
62
|
+
project?: {
|
|
63
|
+
id: string;
|
|
64
|
+
label: string;
|
|
65
|
+
};
|
|
66
|
+
observedModel?: string;
|
|
67
|
+
observedEffort?: string;
|
|
68
|
+
}): WorkSessionDetailFields;
|
|
69
|
+
/** Read only: details for a saved session with no live native process, if its directory still exists. */
|
|
70
|
+
export declare function savedSessionFields(input: {
|
|
71
|
+
state: LocalRuntimeSessionState;
|
|
72
|
+
agentId: string;
|
|
73
|
+
projects: readonly ManagedSessionProject[];
|
|
74
|
+
describeConfigured?: (settings: WorkSessionSettings) => ManagedSessionConfiguredFacts;
|
|
75
|
+
}): WorkSessionDetailFields;
|
|
76
|
+
/**
|
|
77
|
+
* Idempotent configuration for `createCanonManagedSessionHost`. Validates the
|
|
78
|
+
* owner's selection against the catalog, prepares the requested environment
|
|
79
|
+
* (a requested worktree that cannot be created fails instead of falling back)
|
|
80
|
+
* and saves the setup before the broker is acknowledged. A replayed request
|
|
81
|
+
* returns the saved setup; a conversation that already has a native session or
|
|
82
|
+
* a different confirmed setup keeps it.
|
|
83
|
+
*/
|
|
84
|
+
export declare function configureManagedSession(input: {
|
|
85
|
+
request: WorkSessionRequest;
|
|
86
|
+
runtimeId: string;
|
|
87
|
+
agentId: string;
|
|
88
|
+
catalog: WorkSessionCatalog;
|
|
89
|
+
projects: readonly ManagedSessionProject[];
|
|
90
|
+
runtime: ManagedSessionRuntime;
|
|
91
|
+
ownerId?: string;
|
|
92
|
+
ownershipVersion?: number;
|
|
93
|
+
/** False when the runtime cannot apply a catalog permission mode with the selected model. */
|
|
94
|
+
isPermissionModeApplicable?: (permissionMode: string, modelId: string | undefined) => boolean;
|
|
95
|
+
describeConfigured?: (settings: WorkSessionSettings) => ManagedSessionConfiguredFacts;
|
|
96
|
+
}): {
|
|
97
|
+
settings: WorkSessionSettings;
|
|
98
|
+
fields: WorkSessionDetailFields;
|
|
99
|
+
};
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { join, relative } from 'node:path';
|
|
5
|
+
import { buildConversationWorktreeSpec, prepareConversationEnvironment, releaseConversationEnvironment, readLocalRuntimeEntry, saveRuntimeSessionState, } from '@canonmsg/core';
|
|
6
|
+
const SETTING_KEYS = ['projectId', 'modelId', 'reasoningEffort', 'permissionMode', 'executionMode'];
|
|
7
|
+
export const MANAGED_EXECUTION_MODE_LABELS = {
|
|
8
|
+
worktree: 'Git worktree',
|
|
9
|
+
locked: 'Project folder',
|
|
10
|
+
};
|
|
11
|
+
export function buildManagedSessionCatalog(input) {
|
|
12
|
+
const defaults = {
|
|
13
|
+
...(input.projects[0] ? { projectId: input.projects[0].id } : {}),
|
|
14
|
+
...(input.defaultModelId ? { modelId: input.defaultModelId } : {}),
|
|
15
|
+
...(input.defaultReasoningEffort ? { reasoningEffort: input.defaultReasoningEffort } : {}),
|
|
16
|
+
permissionMode: input.defaultPermissionMode, executionMode: input.defaultExecutionMode,
|
|
17
|
+
};
|
|
18
|
+
const catalog = {
|
|
19
|
+
provider: input.provider, canCreate: false, canAttach: false, managedSetup: { version: 1 },
|
|
20
|
+
projects: input.projects.map((project) => ({ id: project.id, label: project.label })),
|
|
21
|
+
models: input.models.map((model) => ({ ...model })),
|
|
22
|
+
...(input.reasoningEfforts?.length ? { reasoningEfforts: input.reasoningEfforts.map((effort) => ({ ...effort })) } : {}),
|
|
23
|
+
permissionModes: input.permissionModes.map((mode) => ({ ...mode })),
|
|
24
|
+
executionModes: input.executionModes.map((id) => ({ id, label: MANAGED_EXECUTION_MODE_LABELS[id] })),
|
|
25
|
+
defaults, sessions: [],
|
|
26
|
+
};
|
|
27
|
+
return { revision: createHash('sha256').update(JSON.stringify(catalog)).digest('hex').slice(0, 24), ...catalog };
|
|
28
|
+
}
|
|
29
|
+
/** The one saved setup for a conversation, or null; conflicting copies fail closed. */
|
|
30
|
+
export function findManagedSessionState(runtimeId, conversationId, runtime) {
|
|
31
|
+
const states = Object.values(readLocalRuntimeEntry(runtimeId)?.sessions ?? {})
|
|
32
|
+
.filter((state) => state.conversationId === conversationId && state.codexManagedSetup);
|
|
33
|
+
if (states.length > 1)
|
|
34
|
+
throw new Error(`Conflicting saved ${runtime.label} setup for this conversation.`);
|
|
35
|
+
return states[0] ?? null;
|
|
36
|
+
}
|
|
37
|
+
export function hasSavedNativeSession(runtimeId, conversationId, resumeField) {
|
|
38
|
+
return Object.values(readLocalRuntimeEntry(runtimeId)?.sessions ?? {})
|
|
39
|
+
.some((state) => state.conversationId === conversationId && Boolean(state[resumeField]));
|
|
40
|
+
}
|
|
41
|
+
/** Older records without ownership fields stay usable; a different owner generation does not. */
|
|
42
|
+
export function savedSessionMatchesOwner(state, ownerId, ownershipVersion) {
|
|
43
|
+
return (state.ownerId === undefined || ownerId === null || state.ownerId === ownerId)
|
|
44
|
+
&& (state.ownershipVersion === undefined || ownershipVersion === undefined
|
|
45
|
+
|| state.ownershipVersion === ownershipVersion);
|
|
46
|
+
}
|
|
47
|
+
export function inspectGitEnvironment(cwd) {
|
|
48
|
+
const fields = {};
|
|
49
|
+
try {
|
|
50
|
+
const branch = execFileSync('git', ['symbolic-ref', '--quiet', '--short', 'HEAD'], { cwd, encoding: 'utf8', timeout: 3_000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
51
|
+
if (branch)
|
|
52
|
+
fields.branch = { value: branch, provenance: 'observed' };
|
|
53
|
+
}
|
|
54
|
+
catch { /* Detached HEAD has no branch to report. */ }
|
|
55
|
+
try {
|
|
56
|
+
const [gitDir, commonDir] = execFileSync('git', ['rev-parse', '--path-format=absolute', '--git-dir', '--git-common-dir'], { cwd, encoding: 'utf8', timeout: 3_000, stdio: ['ignore', 'pipe', 'ignore'] })
|
|
57
|
+
.trim().split(/\r?\n/);
|
|
58
|
+
if (gitDir && commonDir && realpathSync(gitDir) !== realpathSync(commonDir)) {
|
|
59
|
+
const path = execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8', timeout: 3_000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
60
|
+
if (path)
|
|
61
|
+
fields.worktreePath = { value: realpathSync(path), provenance: 'observed' };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch { /* An unavailable Git path has no worktree observation. */ }
|
|
65
|
+
return fields;
|
|
66
|
+
}
|
|
67
|
+
function applyConfiguredFacts(fields, facts) {
|
|
68
|
+
if (facts?.sandbox)
|
|
69
|
+
fields.sandbox = { value: facts.sandbox, provenance: 'configured' };
|
|
70
|
+
if (facts?.approvalPolicy)
|
|
71
|
+
fields.approvalPolicy = { value: facts.approvalPolicy, provenance: 'configured' };
|
|
72
|
+
if (facts?.nativeThreadBehavior) {
|
|
73
|
+
fields.nativeThreadBehavior = { value: facts.nativeThreadBehavior, provenance: 'configured' };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Private owner details for a running session: confirmed settings, then what the runtime and Git observe. */
|
|
77
|
+
export function managedSessionFields(input) {
|
|
78
|
+
const fields = {};
|
|
79
|
+
for (const key of SETTING_KEYS) {
|
|
80
|
+
if (input.settings[key])
|
|
81
|
+
fields[key] = { value: input.settings[key], provenance: 'configured' };
|
|
82
|
+
}
|
|
83
|
+
if (input.projectLabel)
|
|
84
|
+
fields.projectLabel = { value: input.projectLabel, provenance: 'configured' };
|
|
85
|
+
fields.workingDirectory = { value: realpathSync(input.cwd), provenance: 'observed' };
|
|
86
|
+
applyConfiguredFacts(fields, input.configured);
|
|
87
|
+
if (input.observedModel)
|
|
88
|
+
fields.modelId = { value: input.observedModel, provenance: 'observed' };
|
|
89
|
+
if (input.observedEffort)
|
|
90
|
+
fields.reasoningEffort = { value: input.observedEffort, provenance: 'observed' };
|
|
91
|
+
Object.assign(fields, inspectGitEnvironment(input.cwd));
|
|
92
|
+
return fields;
|
|
93
|
+
}
|
|
94
|
+
/** Private details for a running session nobody set up: what the host and Git observe. */
|
|
95
|
+
export function observedSessionFields(input) {
|
|
96
|
+
const fields = { workingDirectory: { value: input.cwd, provenance: 'observed' } };
|
|
97
|
+
if (input.executionMode)
|
|
98
|
+
fields.executionMode = { value: input.executionMode, provenance: 'observed' };
|
|
99
|
+
if (input.project) {
|
|
100
|
+
fields.projectId = { value: input.project.id, provenance: 'observed' };
|
|
101
|
+
fields.projectLabel = { value: input.project.label, provenance: 'observed' };
|
|
102
|
+
}
|
|
103
|
+
if (input.observedModel)
|
|
104
|
+
fields.modelId = { value: input.observedModel, provenance: 'observed' };
|
|
105
|
+
if (input.observedEffort)
|
|
106
|
+
fields.reasoningEffort = { value: input.observedEffort, provenance: 'observed' };
|
|
107
|
+
Object.assign(fields, inspectGitEnvironment(input.cwd));
|
|
108
|
+
return fields;
|
|
109
|
+
}
|
|
110
|
+
/** Read only: details for a saved session with no live native process, if its directory still exists. */
|
|
111
|
+
export function savedSessionFields(input) {
|
|
112
|
+
const { state } = input;
|
|
113
|
+
const configured = state.codexManagedSetup?.settings;
|
|
114
|
+
const project = input.projects.find((entry) => entry.cwd === state.baseCwd);
|
|
115
|
+
let cwd;
|
|
116
|
+
if (state.executionMode === 'locked' && existsSync(state.baseCwd))
|
|
117
|
+
cwd = state.baseCwd;
|
|
118
|
+
if (state.executionMode === 'worktree') {
|
|
119
|
+
try {
|
|
120
|
+
const spec = buildConversationWorktreeSpec({ agentId: input.agentId,
|
|
121
|
+
conversationId: state.conversationId, workspaceCwd: state.baseCwd });
|
|
122
|
+
const root = execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd: state.baseCwd, encoding: 'utf8', timeout: 3_000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
123
|
+
const candidate = join(spec.worktreePath, relative(root, state.baseCwd));
|
|
124
|
+
if (existsSync(candidate))
|
|
125
|
+
cwd = candidate;
|
|
126
|
+
}
|
|
127
|
+
catch { /* Unavailable path stays unreported. */ }
|
|
128
|
+
}
|
|
129
|
+
const facts = configured ? input.describeConfigured?.(configured) : undefined;
|
|
130
|
+
if (configured && cwd) {
|
|
131
|
+
return managedSessionFields({ settings: configured, cwd, projectLabel: project?.label, configured: facts });
|
|
132
|
+
}
|
|
133
|
+
const fields = {};
|
|
134
|
+
if (configured) {
|
|
135
|
+
for (const key of SETTING_KEYS) {
|
|
136
|
+
if (configured[key])
|
|
137
|
+
fields[key] = { value: configured[key], provenance: 'configured' };
|
|
138
|
+
}
|
|
139
|
+
if (project?.label)
|
|
140
|
+
fields.projectLabel = { value: project.label, provenance: 'configured' };
|
|
141
|
+
applyConfiguredFacts(fields, facts);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
if (state.executionMode)
|
|
145
|
+
fields.executionMode = { value: state.executionMode, provenance: 'observed' };
|
|
146
|
+
if (project) {
|
|
147
|
+
fields.projectId = { value: project.id, provenance: 'observed' };
|
|
148
|
+
fields.projectLabel = { value: project.label, provenance: 'observed' };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (cwd) {
|
|
152
|
+
fields.workingDirectory = { value: realpathSync(cwd), provenance: 'observed' };
|
|
153
|
+
Object.assign(fields, inspectGitEnvironment(cwd));
|
|
154
|
+
}
|
|
155
|
+
return fields;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Idempotent configuration for `createCanonManagedSessionHost`. Validates the
|
|
159
|
+
* owner's selection against the catalog, prepares the requested environment
|
|
160
|
+
* (a requested worktree that cannot be created fails instead of falling back)
|
|
161
|
+
* and saves the setup before the broker is acknowledged. A replayed request
|
|
162
|
+
* returns the saved setup; a conversation that already has a native session or
|
|
163
|
+
* a different confirmed setup keeps it.
|
|
164
|
+
*/
|
|
165
|
+
export function configureManagedSession(input) {
|
|
166
|
+
const { runtime } = input;
|
|
167
|
+
const selection = input.request.selection;
|
|
168
|
+
if (selection.mode !== 'configure' || input.catalog.managedSetup?.version !== 1)
|
|
169
|
+
throw new Error('This host does not support the requested setup.');
|
|
170
|
+
const existing = findManagedSessionState(input.runtimeId, input.request.conversationId, runtime);
|
|
171
|
+
if (existing?.codexManagedSetup) {
|
|
172
|
+
if (!savedSessionMatchesOwner(existing, input.ownerId ?? null, input.ownershipVersion))
|
|
173
|
+
throw new Error(`The saved ${runtime.label} setup belongs to a previous owner generation.`);
|
|
174
|
+
if (existing.codexManagedSetup.requestId !== input.request.requestId)
|
|
175
|
+
throw new Error(`This conversation already has a confirmed ${runtime.label} setup.`);
|
|
176
|
+
const settings = existing.codexManagedSetup.settings;
|
|
177
|
+
const environment = prepareConversationEnvironment({ agentId: input.agentId,
|
|
178
|
+
conversationId: input.request.conversationId, workspaceCwd: existing.baseCwd,
|
|
179
|
+
allowWorktrees: existing.executionMode === 'worktree' });
|
|
180
|
+
try {
|
|
181
|
+
if (environment.mode !== existing.executionMode)
|
|
182
|
+
throw new Error(`The saved ${runtime.label} execution environment is unavailable.`);
|
|
183
|
+
return { settings,
|
|
184
|
+
fields: managedSessionFields({ settings, cwd: environment.cwd,
|
|
185
|
+
projectLabel: input.projects.find((project) => project.id === settings.projectId)?.label,
|
|
186
|
+
configured: input.describeConfigured?.(settings) }),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
releaseConversationEnvironment(environment);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (hasSavedNativeSession(input.runtimeId, input.request.conversationId, runtime.resumeField))
|
|
194
|
+
throw new Error(`This conversation already has a native ${runtime.label} ${runtime.nativeSession}. Its settings are preserved.`);
|
|
195
|
+
const project = input.projects.find((entry) => entry.id === selection.projectId);
|
|
196
|
+
if (!project)
|
|
197
|
+
throw new Error('The selected project is unavailable on this host.');
|
|
198
|
+
const modelId = selection.modelId ?? input.catalog.defaults?.modelId;
|
|
199
|
+
const model = input.catalog.models?.find((entry) => entry.id === modelId);
|
|
200
|
+
if (modelId && !model)
|
|
201
|
+
throw new Error('The selected model is unavailable on this host.');
|
|
202
|
+
const reasoningEffort = selection.reasoningEffort ?? input.catalog.defaults?.reasoningEffort;
|
|
203
|
+
if (reasoningEffort && !(model?.reasoningEfforts?.length ? model.reasoningEfforts : input.catalog.reasoningEfforts ?? [])
|
|
204
|
+
.some((entry) => entry.id === reasoningEffort))
|
|
205
|
+
throw new Error('The selected thinking level is unavailable for this model.');
|
|
206
|
+
const permissionMode = selection.permissionMode ?? input.catalog.defaults?.permissionMode;
|
|
207
|
+
const executionMode = selection.executionMode ?? input.catalog.defaults?.executionMode;
|
|
208
|
+
if (!permissionMode || !input.catalog.permissionModes?.some((entry) => entry.id === permissionMode)
|
|
209
|
+
|| !executionMode || !input.catalog.executionModes?.some((entry) => entry.id === executionMode))
|
|
210
|
+
throw new Error('The selected execution policy is unavailable on this host.');
|
|
211
|
+
if (input.isPermissionModeApplicable && !input.isPermissionModeApplicable(permissionMode, modelId))
|
|
212
|
+
throw new Error('The selected permission policy cannot be applied.');
|
|
213
|
+
const environment = prepareConversationEnvironment({ agentId: input.agentId,
|
|
214
|
+
conversationId: input.request.conversationId, workspaceCwd: project.cwd,
|
|
215
|
+
allowWorktrees: executionMode === 'worktree' });
|
|
216
|
+
try {
|
|
217
|
+
if (environment.mode !== executionMode)
|
|
218
|
+
throw new Error('The requested Git worktree could not be created. Select Project folder explicitly to use the base project.');
|
|
219
|
+
const settings = { projectId: project.id,
|
|
220
|
+
...(modelId ? { modelId } : {}), ...(reasoningEffort ? { reasoningEffort } : {}),
|
|
221
|
+
permissionMode, executionMode };
|
|
222
|
+
const fields = managedSessionFields({ settings, cwd: environment.cwd, projectLabel: project.label,
|
|
223
|
+
configured: input.describeConfigured?.(settings) });
|
|
224
|
+
saveRuntimeSessionState(input.runtimeId, { conversationId: input.request.conversationId,
|
|
225
|
+
baseCwd: environment.baseCwd, executionMode: executionMode,
|
|
226
|
+
...(input.ownerId ? { ownerId: input.ownerId } : {}),
|
|
227
|
+
...(input.ownershipVersion !== undefined ? { ownershipVersion: input.ownershipVersion } : {}),
|
|
228
|
+
codexManagedSetup: { requestId: input.request.requestId, settings } });
|
|
229
|
+
return { settings, fields };
|
|
230
|
+
}
|
|
231
|
+
finally {
|
|
232
|
+
releaseConversationEnvironment(environment);
|
|
233
|
+
}
|
|
234
|
+
}
|
package/dist/media.d.ts
CHANGED
|
@@ -82,6 +82,17 @@ export declare const MAX_CANON_MEDIA_BYTES: number;
|
|
|
82
82
|
export declare const MAX_ANTHROPIC_IMAGE_RAW_BYTES: number;
|
|
83
83
|
export declare const MAX_ANTHROPIC_REQUEST_BYTES = 32000000;
|
|
84
84
|
export declare const DEFAULT_ANTHROPIC_REQUEST_HEADROOM_BYTES: number;
|
|
85
|
+
/**
|
|
86
|
+
* The directory every materialized attachment of one conversation lands in
|
|
87
|
+
* (`<root>/<agent>/<conversation>/<message>/…`). A host that runs one native
|
|
88
|
+
* session per conversation can grant its runtime read access to exactly this
|
|
89
|
+
* directory instead of approving each downloaded file.
|
|
90
|
+
*/
|
|
91
|
+
export declare function conversationMediaCacheDir(input: {
|
|
92
|
+
agentId: string;
|
|
93
|
+
conversationId: string;
|
|
94
|
+
rootDir?: string;
|
|
95
|
+
}): string;
|
|
85
96
|
export declare function getMessageAttachments(message: Pick<CanonMessage, 'attachments'>): MediaAttachment[];
|
|
86
97
|
export declare function materializeAttachment(attachment: MediaAttachment, options: MaterializeMediaOptions & {
|
|
87
98
|
index?: number;
|
package/dist/media.js
CHANGED
|
@@ -82,6 +82,15 @@ function inferExtension(input) {
|
|
|
82
82
|
}
|
|
83
83
|
return '.bin';
|
|
84
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* The directory every materialized attachment of one conversation lands in
|
|
87
|
+
* (`<root>/<agent>/<conversation>/<message>/…`). A host that runs one native
|
|
88
|
+
* session per conversation can grant its runtime read access to exactly this
|
|
89
|
+
* directory instead of approving each downloaded file.
|
|
90
|
+
*/
|
|
91
|
+
export function conversationMediaCacheDir(input) {
|
|
92
|
+
return join(input.rootDir ?? DEFAULT_MEDIA_CACHE_DIR, sanitizeSegment(input.agentId, 'agent'), sanitizeSegment(input.conversationId, 'conversation'));
|
|
93
|
+
}
|
|
85
94
|
function buildCachePath(input) {
|
|
86
95
|
const rootDir = input.rootDir ?? DEFAULT_MEDIA_CACHE_DIR;
|
|
87
96
|
const extension = inferExtension({
|
|
@@ -93,7 +102,7 @@ function buildCachePath(input) {
|
|
|
93
102
|
? sanitizeDownloadFileName(input.attachment.fileName, fallbackName)
|
|
94
103
|
: fallbackName;
|
|
95
104
|
const fileName = extname(preferredName) ? preferredName : `${preferredName}${extension}`;
|
|
96
|
-
return join(
|
|
105
|
+
return join(conversationMediaCacheDir({ agentId: input.agentId, conversationId: input.conversationId, rootDir }), sanitizeSegment(input.messageId, 'message'), `${String(input.index).padStart(2, '0')}-${fileName}`);
|
|
97
106
|
}
|
|
98
107
|
function ensureFetch(fetchImpl) {
|
|
99
108
|
if (fetchImpl)
|
package/dist/types.d.ts
CHANGED
|
@@ -207,6 +207,26 @@ export interface MessageHandlerContext {
|
|
|
207
207
|
provenance: CanonRuntimeProvenance;
|
|
208
208
|
/** Compact structured Canon turn context for the latest inbound message in this handler batch. */
|
|
209
209
|
turnContext: CanonTurnContextV2;
|
|
210
|
+
/**
|
|
211
|
+
* Each batched input's own shared-work references (the version it was
|
|
212
|
+
* about, `origin` and any selection), in batch order. Malformed references
|
|
213
|
+
* are dropped; an input with none is omitted.
|
|
214
|
+
*/
|
|
215
|
+
artifactContexts: Array<{
|
|
216
|
+
messageId: string;
|
|
217
|
+
context: import('@canonmsg/core').ArtifactContext;
|
|
218
|
+
}>;
|
|
219
|
+
/**
|
|
220
|
+
* Shared work in this handler's conversation (chess, forms, images): exactly
|
|
221
|
+
* `CanonClient.artifacts(conversationId)`. Changes are made against the state
|
|
222
|
+
* this agent last read or wrote (across turns), so a person's newer edit is
|
|
223
|
+
* reported as a `CanonArtifactError` conflict instead of being overwritten:
|
|
224
|
+
* `describe()` before `update()` (or pass `basedOnRevisionId`); with no read
|
|
225
|
+
* yet, `update()` rejects with `NOT_READ`. Arrival reads nothing on the
|
|
226
|
+
* handler's behalf. Dev-only preview; other environments reject with
|
|
227
|
+
* `ARTIFACTS_UNAVAILABLE`.
|
|
228
|
+
*/
|
|
229
|
+
readonly artifacts: import('@canonmsg/core').CanonConversationArtifacts;
|
|
210
230
|
/** Runtime turn mode requested by the sender for this inbound turn, if any. */
|
|
211
231
|
requestedTurnMode: string | null;
|
|
212
232
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/agent-sdk",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.2.0",
|
|
4
4
|
"description": "Canon Agent SDK — build AI agents that participate in Canon conversations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"node": ">=22.22.3"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@canonmsg/core": "^13.
|
|
31
|
+
"@canonmsg/core": "^13.3.0"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|