@canonmsg/codex-plugin 0.12.0 → 0.13.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.
Files changed (2) hide show
  1. package/dist/host.js +150 -1
  2. package/package.json +3 -3
package/dist/host.js CHANGED
@@ -5,7 +5,7 @@ import { spawnSync } from 'node:child_process';
5
5
  import { dirname } from 'node:path';
6
6
  import { parseArgs } from 'node:util';
7
7
  import { getCodexImagePath, materializeMessageMedia, materializeReplyContextMedia, } from '@canonmsg/agent-sdk';
8
- import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildPlanApprovalRequest, buildCanonHostPrompt, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createRuntimeStatePublisher, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, getActiveProfileLock, initRTDBAuth, buildLocalRuntimeId, heartbeatLocalRuntimeEntry, loadRuntimeSessionState, markLocalRuntimeStopped, normalizeTurnMetadata, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, rtdbRead, rtdbWrite, shouldTriggerAgentTurn, saveRuntimeSessionState, buildBoundedTurnTrail, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
8
+ import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildPlanApprovalRequest, buildCanonHostPrompt, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createRuntimeStatePublisher, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, getActiveProfileLock, initRTDBAuth, buildLocalRuntimeId, heartbeatLocalRuntimeEntry, loadRuntimeSessionState, markLocalRuntimeStopped, normalizeTurnMetadata, parseRuntimeCardV1, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, rtdbRead, rtdbWrite, shouldTriggerAgentTurn, saveRuntimeSessionState, buildBoundedTurnTrail, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
9
9
  import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
10
10
  import { CodexConversationAdapter, } from './adapter.js';
11
11
  import { CodexAppServerAdapter } from './app-server-adapter.js';
@@ -109,6 +109,21 @@ function buildCodexRuntimeDescriptor(input) {
109
109
  presentation: input.presentation,
110
110
  streamingTextMode: 'snapshot',
111
111
  commands,
112
+ ...(input.supportsRichCards
113
+ ? {
114
+ runtimeCards: {
115
+ rich: {
116
+ schema: 'canon.card.v1',
117
+ lifecycle: 'blocking_requires_action',
118
+ responder: 'agent_owner',
119
+ result: 'action_or_values',
120
+ maxTimeoutMs: 30 * 60_000,
121
+ blockKinds: ['summary', 'metricGrid', 'chart', 'table', 'list', 'callout', 'actions'],
122
+ native: true,
123
+ },
124
+ },
125
+ }
126
+ : {}),
112
127
  });
113
128
  if (input.models.length > 0) {
114
129
  return descriptor;
@@ -893,10 +908,141 @@ export async function main() {
893
908
  }).catch(() => null);
894
909
  return { decision: 'deny' };
895
910
  }
911
+ async function waitForRuntimeCardResponse(input) {
912
+ while (Date.now() < input.expiresAt) {
913
+ const response = await client.consumeRuntimeCardResponse({
914
+ conversationId: input.conversationId,
915
+ cardId: input.cardId,
916
+ }).catch(() => null);
917
+ if (response?.status === 'submitted') {
918
+ return {
919
+ status: 'submitted',
920
+ ...(response.actionId ? { actionId: response.actionId } : {}),
921
+ ...(response.values ? { values: response.values } : {}),
922
+ };
923
+ }
924
+ if (response?.status === 'cancelled' || response?.status === 'timeout') {
925
+ return { status: response.status };
926
+ }
927
+ await sleep(1_000);
928
+ }
929
+ const response = await client.consumeRuntimeCardResponse({
930
+ conversationId: input.conversationId,
931
+ cardId: input.cardId,
932
+ }).catch(() => null);
933
+ if (response?.status === 'submitted') {
934
+ return {
935
+ status: 'submitted',
936
+ ...(response.actionId ? { actionId: response.actionId } : {}),
937
+ ...(response.values ? { values: response.values } : {}),
938
+ };
939
+ }
940
+ return { status: 'timeout' };
941
+ }
942
+ function runtimeCardRequestPayload(method, params) {
943
+ if (method !== 'item/runtimeCard/request'
944
+ && method !== 'runtimeCard/request') {
945
+ return null;
946
+ }
947
+ const input = isRecord(params.input) ? params.input : null;
948
+ const args = isRecord(params.arguments) ? params.arguments : null;
949
+ return params.card ?? params.cardDocument ?? input?.card ?? args?.card ?? null;
950
+ }
896
951
  async function handleCodexServerRequest(session, request) {
897
952
  const requestId = String(request.id);
898
953
  const params = request.params;
899
954
  const expiresAt = Date.now() + 30 * 60_000;
955
+ const runtimeCardPayload = runtimeCardRequestPayload(request.method, params);
956
+ if (runtimeCardPayload) {
957
+ const card = parseRuntimeCardV1(runtimeCardPayload);
958
+ if (!card) {
959
+ return { status: 'cancelled', error: 'Invalid canon.card.v1 card' };
960
+ }
961
+ const cardId = readString(params, 'cardId')
962
+ ?? readString(params, 'itemId')
963
+ ?? card.cardId
964
+ ?? requestId;
965
+ let requestCreated = false;
966
+ let requestResolved = false;
967
+ try {
968
+ await client.createRuntimeCardRequest({
969
+ conversationId: session.conversationId,
970
+ cardId,
971
+ card: { ...card, cardId },
972
+ expiresAt,
973
+ // Omit responseUserId so the backend targets a reachable member (the
974
+ // owner if present, else the sole other member) — the owner is often
975
+ // not a member of agent-to-user DMs.
976
+ native: {
977
+ runtime: 'codex',
978
+ method: request.method,
979
+ requestId,
980
+ turnId: readString(params, 'turnId') ?? session.currentTurnId ?? undefined,
981
+ handles: {
982
+ itemId: readString(params, 'itemId') ?? '',
983
+ threadId: readString(params, 'threadId') ?? '',
984
+ },
985
+ },
986
+ turnId: session.currentTurnId ?? undefined,
987
+ });
988
+ requestCreated = true;
989
+ session.turnState = 'waiting_input';
990
+ markTurnProgress(session);
991
+ upsertTurnBlock(session, {
992
+ id: `card:${cardId}`,
993
+ kind: 'input',
994
+ status: 'pending',
995
+ title: card.title,
996
+ summary: card.template ?? 'runtime card',
997
+ });
998
+ writeTurn(session);
999
+ stopVisibleWorkSignal(session);
1000
+ writeCodexStreaming(session, null, 'waiting_input');
1001
+ const response = await waitForRuntimeCardResponse({
1002
+ conversationId: session.conversationId,
1003
+ cardId,
1004
+ expiresAt,
1005
+ });
1006
+ requestResolved = true;
1007
+ const outcome = buildRuntimeCardOutcome(cardId, response.status, { reason: response.status });
1008
+ await client.sendMessage(session.conversationId, outcome.text, {
1009
+ metadata: {
1010
+ ...outcome.metadata,
1011
+ turnId: session.currentTurnId ?? undefined,
1012
+ turnSemantics: 'control',
1013
+ replyBehavior: 'suppress_auto_reply',
1014
+ },
1015
+ });
1016
+ completeTurnBlock(session, `card:${cardId}`, `Card ${response.status}`);
1017
+ if (session.turnState === 'waiting_input') {
1018
+ session.turnState = 'thinking';
1019
+ markTurnProgress(session);
1020
+ writeTurn(session);
1021
+ startVisibleWorkSignal(session);
1022
+ writeCodexStreaming(session, null, 'thinking');
1023
+ }
1024
+ return response;
1025
+ }
1026
+ catch (error) {
1027
+ if (requestCreated && !requestResolved) {
1028
+ await client.consumeRuntimeCardResponse({
1029
+ conversationId: session.conversationId,
1030
+ cardId,
1031
+ cancel: true,
1032
+ }).catch(() => null);
1033
+ const outcome = buildRuntimeCardOutcome(cardId, 'cancelled', { reason: 'interrupted' });
1034
+ await client.sendMessage(session.conversationId, outcome.text, {
1035
+ metadata: {
1036
+ ...outcome.metadata,
1037
+ turnId: session.currentTurnId ?? undefined,
1038
+ turnSemantics: 'control',
1039
+ replyBehavior: 'suppress_auto_reply',
1040
+ },
1041
+ }).catch(() => null);
1042
+ }
1043
+ throw error;
1044
+ }
1045
+ }
900
1046
  if (request.method === 'item/tool/requestUserInput') {
901
1047
  const paramsInput = isRecord(params.input) ? params.input : null;
902
1048
  const paramsArguments = isRecord(params.arguments) ? params.arguments : null;
@@ -1347,6 +1493,7 @@ export async function main() {
1347
1493
  defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode,
1348
1494
  presentation: runtimePresentation,
1349
1495
  supportsPlanMode: useAppServer,
1496
+ supportsRichCards: useAppServer,
1350
1497
  }),
1351
1498
  };
1352
1499
  async function baselineControlSignal(conversationId) {
@@ -1542,6 +1689,7 @@ export async function main() {
1542
1689
  defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode,
1543
1690
  presentation: runtimePresentation,
1544
1691
  supportsPlanMode: useAppServer,
1692
+ supportsRichCards: useAppServer,
1545
1693
  }),
1546
1694
  };
1547
1695
  }
@@ -1563,6 +1711,7 @@ export async function main() {
1563
1711
  defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode,
1564
1712
  presentation: runtimePresentation,
1565
1713
  supportsPlanMode: useAppServer,
1714
+ supportsRichCards: useAppServer,
1566
1715
  }),
1567
1716
  };
1568
1717
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/codex-plugin",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Canon host integration for Codex CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,8 +29,8 @@
29
29
  "prepack": "npm run build"
30
30
  },
31
31
  "dependencies": {
32
- "@canonmsg/agent-sdk": "^2.0.0",
33
- "@canonmsg/core": "^1.0.0"
32
+ "@canonmsg/agent-sdk": "^2.1.0",
33
+ "@canonmsg/core": "^1.2.0"
34
34
  },
35
35
  "engines": {
36
36
  "node": ">=18.0.0"