@athenaintel/react 0.37.0 → 0.38.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/dist/agent-runtime/src/messages.cjs +27 -1
- package/dist/agent-runtime/src/messages.cjs.map +1 -1
- package/dist/agent-runtime/src/messages.js +27 -1
- package/dist/agent-runtime/src/messages.js.map +1 -1
- package/dist/agent-runtime/src/queue.cjs +4 -0
- package/dist/agent-runtime/src/queue.cjs.map +1 -1
- package/dist/agent-runtime/src/queue.js +4 -0
- package/dist/agent-runtime/src/queue.js.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-config.cjs +2 -1
- package/dist/agent-runtime/src/use-deep-agent-config.cjs.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-config.js +2 -1
- package/dist/agent-runtime/src/use-deep-agent-config.js.map +1 -1
- package/dist/auth/AthenaSSOProvider.cjs +202 -103
- package/dist/auth/AthenaSSOProvider.cjs.map +1 -1
- package/dist/auth/AthenaSSOProvider.d.ts +35 -4
- package/dist/auth/AthenaSSOProvider.js +202 -103
- package/dist/auth/AthenaSSOProvider.js.map +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/chat/AthenaChat.cjs +1 -1
- package/dist/chat/AthenaChat.cjs.map +1 -1
- package/dist/chat/AthenaChat.js +1 -1
- package/dist/chat/AthenaChat.js.map +1 -1
- package/dist/chat/StatewireQueuedMessages.cjs +18 -0
- package/dist/chat/StatewireQueuedMessages.cjs.map +1 -1
- package/dist/chat/StatewireQueuedMessages.d.ts +19 -1
- package/dist/chat/StatewireQueuedMessages.js +20 -2
- package/dist/chat/StatewireQueuedMessages.js.map +1 -1
- package/dist/diagnostics/athena-diagnostics.cjs.map +1 -1
- package/dist/diagnostics/athena-diagnostics.d.ts +1 -1
- package/dist/diagnostics/athena-diagnostics.js.map +1 -1
- package/dist/diagnostics/errors.cjs +7 -0
- package/dist/diagnostics/errors.cjs.map +1 -1
- package/dist/diagnostics/errors.d.ts +7 -0
- package/dist/diagnostics/errors.js +7 -0
- package/dist/diagnostics/errors.js.map +1 -1
- package/dist/react/package.json.cjs +1 -1
- package/dist/react/package.json.js +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-deep-agent-config.cjs","sources":["../../../../agent-runtime/src/use-deep-agent-config.ts"],"sourcesContent":["import { useStatewireSendCommand, useStatewireState } from '@assistant-ui/react-statewire';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport {\n applyConfigPatch,\n applyToolsDisable,\n applyToolsEnable,\n type ThreadConfigCustom,\n type ThreadConfigPatch,\n} from './config-mutations';\nimport { statewireRejectionPayload } from './errors';\nimport { DEEP_AGENT_CONFIG_COMMANDS } from './generated/runconfig-contract';\nimport { readDeepAgentStateThreadId } from './statewire-state';\n\ndeclare module '@assistant-ui/core' {\n namespace Assistant {\n interface Commands {\n /** Set / unset top-level keys of the thread's server config (`null` unsets). */\n configPatch: {\n type: typeof DEEP_AGENT_CONFIG_COMMANDS.patch;\n patch: Record<string, unknown>;\n };\n /** Append ids to the thread's `enabled_tools`. */\n configToolsEnable: {\n type: typeof DEEP_AGENT_CONFIG_COMMANDS.toolsEnable;\n toolIds: string[];\n };\n /** Remove ids from the thread's `enabled_tools`. */\n configToolsDisable: {\n type: typeof DEEP_AGENT_CONFIG_COMMANDS.toolsDisable;\n toolIds: string[];\n };\n }\n }\n}\n\n/**\n * The replicated `state.config` lane of a thread with server-authoritative\n * run config. Absent — and `readServerConfig` returns `null` — while the\n * thread has never been configured (version 0: a brand-new thread, or one\n * created by a client predating the lane).\n */\nexport interface DeepAgentServerConfig {\n /** Monotonic server-incremented version; always >= 1 when the lane exists. */\n version: number;\n /** The thread-config keys every run on this thread uses as its base. */\n custom: ThreadConfigCustom;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n/**\n * Read the server-config lane off a raw statewire state.\n *\n * Identity-preserving (returns the raw `state.config` reference), so it is\n * safe as a `useStatewireState` selector: the reference only changes when the\n * server writes the lane.\n */\nexport function readServerConfig(state: unknown): DeepAgentServerConfig | null {\n const config = (state as { config?: unknown } | null | undefined)?.config;\n if (!isRecord(config)) return null;\n const version = config.version;\n if (typeof version !== 'number' || !Number.isInteger(version) || version <= 0) return null;\n if (!isRecord(config.custom)) return null;\n return config as unknown as DeepAgentServerConfig;\n}\n\n/** The thread's server config version: 0 while the thread has never been configured. */\nexport function readConfigVersion(state: unknown): number {\n return readServerConfig(state)?.version ?? 0;\n}\n\n/**\n * {@link readServerConfig}, but only from a state `threadId`'s own attach\n * produced (`readDeepAgentStateThreadId`); any other state reads as\n * unconfigured. With no `threadId` it is {@link readServerConfig} unchanged.\n *\n * Identity-preserving like {@link readServerConfig}, so it is safe as a\n * `useStatewireState` selector.\n */\nexport function readThreadServerConfig({\n state,\n threadId,\n}: {\n state: unknown;\n threadId: string | undefined;\n}): DeepAgentServerConfig | null {\n if (threadId !== undefined && readDeepAgentStateThreadId(state) !== threadId) return null;\n return readServerConfig(state);\n}\n\n/**\n * The 403 rejection codes a view-only collaborator's `config/*` write can\n * come back with: `config_write_forbidden` from the config host's own gate,\n * or `thread_write_forbidden` from the host-wide mutation gate that runs\n * ahead of it and refuses every thread-mutating command on a view grant.\n */\nconst CONFIG_WRITE_FORBIDDEN_CODES: ReadonlySet<string> = new Set([\n 'config_write_forbidden',\n 'thread_write_forbidden',\n]);\n\n/**\n * Whether a rejected `config/*` command was refused because the submitter\n * only has view access to the session. The write is not retriable by that\n * user; hosts disable their config controls.\n */\nexport function isConfigWriteForbiddenRejection(error: unknown): boolean {\n const payload = statewireRejectionPayload(error);\n if (payload?.status !== 403) return false;\n const detail = payload.detail as { code?: unknown } | undefined;\n return typeof detail?.code === 'string' && CONFIG_WRITE_FORBIDDEN_CODES.has(detail.code);\n}\n\n/**\n * Whether a rejected `config/*` command was refused because the thread's\n * workspace cannot hold server config at all (`config_sync_unavailable`, a\n * regionally-pinned workspace). No write will ever succeed on the thread;\n * hosts stop issuing them and send their full config per run instead.\n */\nexport function isConfigSyncUnavailableRejection(error: unknown): boolean {\n const payload = statewireRejectionPayload(error);\n if (payload?.status !== 400) return false;\n const detail = payload.detail as { code?: unknown } | undefined;\n return detail?.code === 'config_sync_unavailable';\n}\n\n/** The settled result of a `config/*` command: the new server config. */\nexport interface DeepAgentConfigWriteResult {\n version: number;\n custom: ThreadConfigCustom;\n}\n\n/** An observed server-config version, delivered to the host callback. */\nexport interface DeepAgentServerConfigEvent {\n version: number;\n /** The server config with this client's still-pending writes folded over. */\n custom: ThreadConfigCustom;\n}\n\ninterface PendingOp {\n id: number;\n apply: (custom: ThreadConfigCustom) => ThreadConfigCustom;\n /** The version the server assigned this op; retired once the lane reaches it. */\n settledVersion: number | null;\n}\n\n/**\n * Fold the pending optimistic ops over the server config — the effective\n * config a client shows and sends against. Pure, for the tests.\n */\nexport function foldPendingConfigOps(\n serverConfig: DeepAgentServerConfig | null,\n ops: readonly Pick<PendingOp, 'apply' | 'settledVersion'>[]\n): ThreadConfigCustom {\n const version = serverConfig?.version ?? 0;\n let custom: ThreadConfigCustom = serverConfig?.custom ?? {};\n for (const op of ops) {\n if (op.settledVersion !== null && op.settledVersion <= version) continue;\n custom = op.apply(custom);\n }\n return custom;\n}\n\nexport interface UseDeepAgentConfigOptions {\n /**\n * Fired whenever the server writes a new version of the lane (another\n * device, the seed, or the echo of our own command), with this client's\n * still-pending writes folded over — hosts mirror it into their local\n * store; nothing is ever clobbered because pending edits stay on top.\n */\n onServerConfig?: (event: DeepAgentServerConfigEvent) => void;\n /**\n * The thread this binding serves. When set, the lane is read only from a\n * state that thread's own attach produced ({@link readThreadServerConfig}):\n * for the first render after a thread switch the client still projects the\n * previous thread's state, and adopting its lane would report the previous\n * thread's config and version as this thread's.\n */\n threadId?: string;\n}\n\nexport interface UseDeepAgentConfigResult {\n /** Reactive server config version (0 while the thread has never been configured). */\n configVersion: number;\n /** Reactive server-config lane, or `null` at version 0. */\n serverConfig: DeepAgentServerConfig | null;\n /**\n * The effective thread config: the server lane with this client's pending\n * writes folded over. `{}` at version 0 with nothing pending.\n */\n custom: ThreadConfigCustom;\n /** `true` while at least one `config/*` command is unacknowledged by the server. */\n hasPendingWrites: boolean;\n /** `config/patch` — set / unset top-level keys; `null` unsets. */\n patchConfig: (patch: ThreadConfigPatch) => Promise<DeepAgentConfigWriteResult>;\n /** `config/tools/enable`. */\n enableTools: (toolIds: readonly string[]) => Promise<DeepAgentConfigWriteResult>;\n /** `config/tools/disable`. */\n disableTools: (toolIds: readonly string[]) => Promise<DeepAgentConfigWriteResult>;\n}\n\ntype ConfigCommand =\n | { type: typeof DEEP_AGENT_CONFIG_COMMANDS.patch; patch: Record<string, unknown> }\n | { type: typeof DEEP_AGENT_CONFIG_COMMANDS.toolsEnable; toolIds: string[] }\n | { type: typeof DEEP_AGENT_CONFIG_COMMANDS.toolsDisable; toolIds: string[] };\n\n/**\n * The server-authoritative thread config of a deep-agent thread.\n *\n * Every capability a run depends on — model, agent binding, enabled tools,\n * sandbox, environment, … — lives on the thread's server config lane and is\n * changed only through the `config/*` commands. Sends carry per-send keys\n * only (`pickPerSendCustom`). The commands are applied optimistically: each\n * one folds its transform over the server config the moment it is issued\n * (`custom`), and retires once the lane reaches the version the server\n * assigned it; a rejection drops it, so the UI snaps back to the truth.\n *\n * Must be mounted under the thread's statewire context (an `AuiProvider`\n * holding `useDeepAgentThread`'s element), keyed by the thread identity so a\n * thread switch remounts it with an empty pending set. A binding that persists\n * what it reads per thread passes `threadId`, so the render that still sees\n * the previous thread's state after a switch reads no lane at all.\n */\nexport function useDeepAgentConfig({\n threadId,\n ...options\n}: UseDeepAgentConfigOptions = {}): UseDeepAgentConfigResult {\n const sendCommand = useStatewireSendCommand();\n const selectServerConfig = useCallback(\n (state: unknown) => readThreadServerConfig({ state, threadId }),\n [threadId]\n );\n const serverConfig = useStatewireState(selectServerConfig);\n return useDeepAgentConfigController({\n serverConfig,\n sendCommand: (command) => Promise.resolve(sendCommand(command as never)),\n ...options,\n });\n}\n\nexport interface DeepAgentConfigControllerInputs extends UseDeepAgentConfigOptions {\n serverConfig: DeepAgentServerConfig | null;\n sendCommand: (command: ConfigCommand) => Promise<unknown>;\n}\n\n/**\n * The transport-agnostic body of {@link useDeepAgentConfig}, with the reactive\n * inputs injected — so the optimistic-overlay behavior is testable without a\n * statewire session.\n */\nexport function useDeepAgentConfigController({\n serverConfig,\n sendCommand,\n onServerConfig,\n}: DeepAgentConfigControllerInputs): UseDeepAgentConfigResult {\n const [ops, setOps] = useState<readonly PendingOp[]>([]);\n const nextOpIdRef = useRef(1);\n const sendCommandRef = useRef(sendCommand);\n sendCommandRef.current = sendCommand;\n const onServerConfigRef = useRef(onServerConfig);\n onServerConfigRef.current = onServerConfig;\n\n const custom = useMemo(() => foldPendingConfigOps(serverConfig, ops), [serverConfig, ops]);\n\n // Retire ops the lane has caught up with (their transform is now part of\n // the server config itself).\n const serverVersion = serverConfig?.version ?? 0;\n const serverVersionRef = useRef(serverVersion);\n serverVersionRef.current = serverVersion;\n useEffect(() => {\n setOps((current) => {\n const kept = current.filter(\n (op) => op.settledVersion === null || op.settledVersion > serverVersion\n );\n return kept.length === current.length ? current : kept;\n });\n }, [serverVersion]);\n\n const lastNotifiedRef = useRef<DeepAgentServerConfig | null>(null);\n useEffect(() => {\n if (serverConfig === null || serverConfig === lastNotifiedRef.current) return;\n lastNotifiedRef.current = serverConfig;\n onServerConfigRef.current?.({ version: serverConfig.version, custom });\n }, [serverConfig, custom]);\n\n const run = useCallback(\n async (\n command: ConfigCommand,\n apply: PendingOp['apply']\n ): Promise<DeepAgentConfigWriteResult> => {\n const id = nextOpIdRef.current++;\n setOps((current) => [...current, { id, apply, settledVersion: null }]);\n try {\n const raw = (await sendCommandRef.current(command)) as\n | { version?: unknown; custom?: unknown }\n | null\n | undefined;\n const version = typeof raw?.version === 'number' ? raw.version : Number.POSITIVE_INFINITY;\n // The lane echo may have landed before the acknowledgement: retire\n // now if so, otherwise stamp the version for the retire effect.\n setOps((current) =>\n version <= serverVersionRef.current\n ? current.filter((op) => op.id !== id)\n : current.map((op) => (op.id === id ? { ...op, settledVersion: version } : op))\n );\n return {\n version: Number.isFinite(version) ? version : 0,\n custom: isRecord(raw?.custom) ? raw.custom : apply({}),\n };\n } catch (error) {\n setOps((current) => current.filter((op) => op.id !== id));\n throw error;\n }\n },\n []\n );\n\n const patchConfig = useCallback(\n (patch: ThreadConfigPatch) =>\n run({ type: DEEP_AGENT_CONFIG_COMMANDS.patch, patch }, (base) =>\n applyConfigPatch(base, patch)\n ),\n [run]\n );\n const enableTools = useCallback(\n (toolIds: readonly string[]) =>\n run({ type: DEEP_AGENT_CONFIG_COMMANDS.toolsEnable, toolIds: [...toolIds] }, (base) =>\n applyToolsEnable(base, toolIds)\n ),\n [run]\n );\n const disableTools = useCallback(\n (toolIds: readonly string[]) =>\n run({ type: DEEP_AGENT_CONFIG_COMMANDS.toolsDisable, toolIds: [...toolIds] }, (base) =>\n applyToolsDisable(base, toolIds)\n ),\n [run]\n );\n\n return {\n configVersion: serverVersion,\n serverConfig,\n custom,\n hasPendingWrites: ops.some((op) => op.settledVersion === null),\n patchConfig,\n enableTools,\n disableTools,\n };\n}\n"],"names":["statewireRejectionPayload","useState","useRef","useMemo","useEffect","useCallback","DEEP_AGENT_CONFIG_COMMANDS","applyConfigPatch","applyToolsEnable","applyToolsDisable"],"mappings":";;;;;;;AAgDA,MAAM,WAAW,CAAC,UAChB,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAS9D,SAAS,iBAAiB,OAA8C;AAC7E,QAAM,SAAU,+BAAmD;AACnE,MAAI,CAAC,SAAS,MAAM,EAAG,QAAO;AAC9B,QAAM,UAAU,OAAO;AACvB,MAAI,OAAO,YAAY,YAAY,CAAC,OAAO,UAAU,OAAO,KAAK,WAAW,EAAG,QAAO;AACtF,MAAI,CAAC,SAAS,OAAO,MAAM,EAAG,QAAO;AACrC,SAAO;AACT;AAgCA,MAAM,mDAAwD,IAAI;AAAA,EAChE;AAAA,EACA;AACF,CAAC;AAOM,SAAS,gCAAgC,OAAyB;AACvE,QAAM,UAAUA,OAAAA,0BAA0B,KAAK;AAC/C,OAAI,mCAAS,YAAW,IAAK,QAAO;AACpC,QAAM,SAAS,QAAQ;AACvB,SAAO,QAAO,iCAAQ,UAAS,YAAY,6BAA6B,IAAI,OAAO,IAAI;AACzF;AAQO,SAAS,iCAAiC,OAAyB;AACxE,QAAM,UAAUA,OAAAA,0BAA0B,KAAK;AAC/C,OAAI,mCAAS,YAAW,IAAK,QAAO;AACpC,QAAM,SAAS,QAAQ;AACvB,UAAO,iCAAQ,UAAS;AAC1B;AA0BO,SAAS,qBACd,cACA,KACoB;AACpB,QAAM,WAAU,6CAAc,YAAW;AACzC,MAAI,UAA6B,6CAAc,WAAU,CAAA;AACzD,aAAW,MAAM,KAAK;AACpB,QAAI,GAAG,mBAAmB,QAAQ,GAAG,kBAAkB,QAAS;AAChE,aAAS,GAAG,MAAM,MAAM;AAAA,EAC1B;AACA,SAAO;AACT;AAyFO,SAAS,6BAA6B;AAAA,EAC3C;AAAA,EACA;AAAA,EACA;AACF,GAA8D;AAC5D,QAAM,CAAC,KAAK,MAAM,IAAIC,MAAAA,SAA+B,CAAA,CAAE;AACvD,QAAM,cAAcC,MAAAA,OAAO,CAAC;AAC5B,QAAM,iBAAiBA,MAAAA,OAAO,WAAW;AACzC,iBAAe,UAAU;AACzB,QAAM,oBAAoBA,MAAAA,OAAO,cAAc;AAC/C,oBAAkB,UAAU;AAE5B,QAAM,SAASC,cAAQ,MAAM,qBAAqB,cAAc,GAAG,GAAG,CAAC,cAAc,GAAG,CAAC;AAIzF,QAAM,iBAAgB,6CAAc,YAAW;AAC/C,QAAM,mBAAmBD,MAAAA,OAAO,aAAa;AAC7C,mBAAiB,UAAU;AAC3BE,QAAAA,UAAU,MAAM;AACd,WAAO,CAAC,YAAY;AAClB,YAAM,OAAO,QAAQ;AAAA,QACnB,CAAC,OAAO,GAAG,mBAAmB,QAAQ,GAAG,iBAAiB;AAAA,MAAA;AAE5D,aAAO,KAAK,WAAW,QAAQ,SAAS,UAAU;AAAA,IACpD,CAAC;AAAA,EACH,GAAG,CAAC,aAAa,CAAC;AAElB,QAAM,kBAAkBF,MAAAA,OAAqC,IAAI;AACjEE,QAAAA,UAAU,MAAM;;AACd,QAAI,iBAAiB,QAAQ,iBAAiB,gBAAgB,QAAS;AACvE,oBAAgB,UAAU;AAC1B,4BAAkB,YAAlB,2CAA4B,EAAE,SAAS,aAAa,SAAS;EAC/D,GAAG,CAAC,cAAc,MAAM,CAAC;AAEzB,QAAM,MAAMC,MAAAA;AAAAA,IACV,OACE,SACA,UACwC;AACxC,YAAM,KAAK,YAAY;AACvB,aAAO,CAAC,YAAY,CAAC,GAAG,SAAS,EAAE,IAAI,OAAO,gBAAgB,KAAA,CAAM,CAAC;AACrE,UAAI;AACF,cAAM,MAAO,MAAM,eAAe,QAAQ,OAAO;AAIjD,cAAM,UAAU,QAAO,2BAAK,aAAY,WAAW,IAAI,UAAU,OAAO;AAGxE;AAAA,UAAO,CAAC,YACN,WAAW,iBAAiB,UACxB,QAAQ,OAAO,CAAC,OAAO,GAAG,OAAO,EAAE,IACnC,QAAQ,IAAI,CAAC,OAAQ,GAAG,OAAO,KAAK,EAAE,GAAG,IAAI,gBAAgB,QAAA,IAAY,EAAG;AAAA,QAAA;AAElF,eAAO;AAAA,UACL,SAAS,OAAO,SAAS,OAAO,IAAI,UAAU;AAAA,UAC9C,QAAQ,SAAS,2BAAK,MAAM,IAAI,IAAI,SAAS,MAAM,CAAA,CAAE;AAAA,QAAA;AAAA,MAEzD,SAAS,OAAO;AACd,eAAO,CAAC,YAAY,QAAQ,OAAO,CAAC,OAAO,GAAG,OAAO,EAAE,CAAC;AACxD,cAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,CAAA;AAAA,EAAC;AAGH,QAAM,cAAcA,MAAAA;AAAAA,IAClB,CAAC,UACC;AAAA,MAAI,EAAE,MAAMC,kBAAAA,2BAA2B,OAAO,MAAA;AAAA,MAAS,CAAC,SACtDC,iCAAiB,MAAM,KAAK;AAAA,IAAA;AAAA,IAEhC,CAAC,GAAG;AAAA,EAAA;AAEN,QAAM,cAAcF,MAAAA;AAAAA,IAClB,CAAC,YACC;AAAA,MAAI,EAAE,MAAMC,kBAAAA,2BAA2B,aAAa,SAAS,CAAC,GAAG,OAAO,EAAA;AAAA,MAAK,CAAC,SAC5EE,iCAAiB,MAAM,OAAO;AAAA,IAAA;AAAA,IAElC,CAAC,GAAG;AAAA,EAAA;AAEN,QAAM,eAAeH,MAAAA;AAAAA,IACnB,CAAC,YACC;AAAA,MAAI,EAAE,MAAMC,kBAAAA,2BAA2B,cAAc,SAAS,CAAC,GAAG,OAAO,EAAA;AAAA,MAAK,CAAC,SAC7EG,kCAAkB,MAAM,OAAO;AAAA,IAAA;AAAA,IAEnC,CAAC,GAAG;AAAA,EAAA;AAGN,SAAO;AAAA,IACL,eAAe;AAAA,IACf;AAAA,IACA;AAAA,IACA,kBAAkB,IAAI,KAAK,CAAC,OAAO,GAAG,mBAAmB,IAAI;AAAA,IAC7D;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;;;;;;"}
|
|
1
|
+
{"version":3,"file":"use-deep-agent-config.cjs","sources":["../../../../agent-runtime/src/use-deep-agent-config.ts"],"sourcesContent":["import { useStatewireSendCommand, useStatewireState } from '@assistant-ui/react-statewire';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport {\n applyConfigPatch,\n applyToolsDisable,\n applyToolsEnable,\n type ThreadConfigCustom,\n type ThreadConfigPatch,\n} from './config-mutations';\nimport { statewireRejectionPayload } from './errors';\nimport { DEEP_AGENT_CONFIG_COMMANDS } from './generated/runconfig-contract';\nimport { readDeepAgentStateThreadId } from './statewire-state';\n\ndeclare module '@assistant-ui/core' {\n namespace Assistant {\n interface Commands {\n /** Set / unset top-level keys of the thread's server config (`null` unsets). */\n configPatch: {\n type: typeof DEEP_AGENT_CONFIG_COMMANDS.patch;\n patch: Record<string, unknown>;\n };\n /** Append ids to the thread's `enabled_tools`. */\n configToolsEnable: {\n type: typeof DEEP_AGENT_CONFIG_COMMANDS.toolsEnable;\n toolIds: string[];\n };\n /** Remove ids from the thread's `enabled_tools`. */\n configToolsDisable: {\n type: typeof DEEP_AGENT_CONFIG_COMMANDS.toolsDisable;\n toolIds: string[];\n };\n }\n }\n}\n\n/**\n * The replicated `state.config` lane of a thread with server-authoritative\n * run config. Absent — and `readServerConfig` returns `null` — while the\n * thread has never been configured (version 0: a brand-new thread, or one\n * created by a client predating the lane).\n */\nexport interface DeepAgentServerConfig {\n /** Monotonic server-incremented version; always >= 1 when the lane exists. */\n version: number;\n /** The thread-config keys every run on this thread uses as its base. */\n custom: ThreadConfigCustom;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n/**\n * Read the server-config lane off a raw statewire state.\n *\n * Identity-preserving (returns the raw `state.config` reference), so it is\n * safe as a `useStatewireState` selector: the reference only changes when the\n * server writes the lane.\n */\nexport function readServerConfig(state: unknown): DeepAgentServerConfig | null {\n const config = (state as { config?: unknown } | null | undefined)?.config;\n if (!isRecord(config)) return null;\n const version = config.version;\n if (typeof version !== 'number' || !Number.isInteger(version) || version <= 0) return null;\n if (!isRecord(config.custom)) return null;\n return config as unknown as DeepAgentServerConfig;\n}\n\n/** The thread's server config version: 0 while the thread has never been configured. */\nexport function readConfigVersion(state: unknown): number {\n return readServerConfig(state)?.version ?? 0;\n}\n\n/**\n * {@link readServerConfig}, but only from a state `threadId`'s own attach\n * produced (`readDeepAgentStateThreadId`); any other state reads as\n * unconfigured. With no `threadId` it is {@link readServerConfig} unchanged.\n *\n * Identity-preserving like {@link readServerConfig}, so it is safe as a\n * `useStatewireState` selector.\n */\nexport function readThreadServerConfig({\n state,\n threadId,\n}: {\n state: unknown;\n threadId: string | undefined;\n}): DeepAgentServerConfig | null {\n if (threadId !== undefined && readDeepAgentStateThreadId(state) !== threadId) return null;\n return readServerConfig(state);\n}\n\n/**\n * The 403 rejection codes a view-only collaborator's `config/*` write can\n * come back with: `config_write_forbidden` from the config host's own gate,\n * or `thread_write_forbidden` from the host-wide mutation gate that runs\n * ahead of it and refuses every thread-mutating command on a view grant.\n */\nconst CONFIG_WRITE_FORBIDDEN_CODES: ReadonlySet<string> = new Set([\n 'config_write_forbidden',\n 'thread_write_forbidden',\n]);\n\n/**\n * Whether a rejected `config/*` command was refused because the submitter\n * only has view access to the session, or because the conversation is\n * archived (`conversation_archived`, a 409: read-only until it is restored).\n * The write is not retriable by that user; hosts disable their config\n * controls.\n */\nexport function isConfigWriteForbiddenRejection(error: unknown): boolean {\n const payload = statewireRejectionPayload(error);\n const detail = payload?.detail as { code?: unknown } | undefined;\n if (payload?.status === 409) return detail?.code === 'conversation_archived';\n if (payload?.status !== 403) return false;\n return typeof detail?.code === 'string' && CONFIG_WRITE_FORBIDDEN_CODES.has(detail.code);\n}\n\n/**\n * Whether a rejected `config/*` command was refused because the thread's\n * workspace cannot hold server config at all (`config_sync_unavailable`, a\n * regionally-pinned workspace). No write will ever succeed on the thread;\n * hosts stop issuing them and send their full config per run instead.\n */\nexport function isConfigSyncUnavailableRejection(error: unknown): boolean {\n const payload = statewireRejectionPayload(error);\n if (payload?.status !== 400) return false;\n const detail = payload.detail as { code?: unknown } | undefined;\n return detail?.code === 'config_sync_unavailable';\n}\n\n/** The settled result of a `config/*` command: the new server config. */\nexport interface DeepAgentConfigWriteResult {\n version: number;\n custom: ThreadConfigCustom;\n}\n\n/** An observed server-config version, delivered to the host callback. */\nexport interface DeepAgentServerConfigEvent {\n version: number;\n /** The server config with this client's still-pending writes folded over. */\n custom: ThreadConfigCustom;\n}\n\ninterface PendingOp {\n id: number;\n apply: (custom: ThreadConfigCustom) => ThreadConfigCustom;\n /** The version the server assigned this op; retired once the lane reaches it. */\n settledVersion: number | null;\n}\n\n/**\n * Fold the pending optimistic ops over the server config — the effective\n * config a client shows and sends against. Pure, for the tests.\n */\nexport function foldPendingConfigOps(\n serverConfig: DeepAgentServerConfig | null,\n ops: readonly Pick<PendingOp, 'apply' | 'settledVersion'>[]\n): ThreadConfigCustom {\n const version = serverConfig?.version ?? 0;\n let custom: ThreadConfigCustom = serverConfig?.custom ?? {};\n for (const op of ops) {\n if (op.settledVersion !== null && op.settledVersion <= version) continue;\n custom = op.apply(custom);\n }\n return custom;\n}\n\nexport interface UseDeepAgentConfigOptions {\n /**\n * Fired whenever the server writes a new version of the lane (another\n * device, the seed, or the echo of our own command), with this client's\n * still-pending writes folded over — hosts mirror it into their local\n * store; nothing is ever clobbered because pending edits stay on top.\n */\n onServerConfig?: (event: DeepAgentServerConfigEvent) => void;\n /**\n * The thread this binding serves. When set, the lane is read only from a\n * state that thread's own attach produced ({@link readThreadServerConfig}):\n * for the first render after a thread switch the client still projects the\n * previous thread's state, and adopting its lane would report the previous\n * thread's config and version as this thread's.\n */\n threadId?: string;\n}\n\nexport interface UseDeepAgentConfigResult {\n /** Reactive server config version (0 while the thread has never been configured). */\n configVersion: number;\n /** Reactive server-config lane, or `null` at version 0. */\n serverConfig: DeepAgentServerConfig | null;\n /**\n * The effective thread config: the server lane with this client's pending\n * writes folded over. `{}` at version 0 with nothing pending.\n */\n custom: ThreadConfigCustom;\n /** `true` while at least one `config/*` command is unacknowledged by the server. */\n hasPendingWrites: boolean;\n /** `config/patch` — set / unset top-level keys; `null` unsets. */\n patchConfig: (patch: ThreadConfigPatch) => Promise<DeepAgentConfigWriteResult>;\n /** `config/tools/enable`. */\n enableTools: (toolIds: readonly string[]) => Promise<DeepAgentConfigWriteResult>;\n /** `config/tools/disable`. */\n disableTools: (toolIds: readonly string[]) => Promise<DeepAgentConfigWriteResult>;\n}\n\ntype ConfigCommand =\n | { type: typeof DEEP_AGENT_CONFIG_COMMANDS.patch; patch: Record<string, unknown> }\n | { type: typeof DEEP_AGENT_CONFIG_COMMANDS.toolsEnable; toolIds: string[] }\n | { type: typeof DEEP_AGENT_CONFIG_COMMANDS.toolsDisable; toolIds: string[] };\n\n/**\n * The server-authoritative thread config of a deep-agent thread.\n *\n * Every capability a run depends on — model, agent binding, enabled tools,\n * sandbox, environment, … — lives on the thread's server config lane and is\n * changed only through the `config/*` commands. Sends carry per-send keys\n * only (`pickPerSendCustom`). The commands are applied optimistically: each\n * one folds its transform over the server config the moment it is issued\n * (`custom`), and retires once the lane reaches the version the server\n * assigned it; a rejection drops it, so the UI snaps back to the truth.\n *\n * Must be mounted under the thread's statewire context (an `AuiProvider`\n * holding `useDeepAgentThread`'s element), keyed by the thread identity so a\n * thread switch remounts it with an empty pending set. A binding that persists\n * what it reads per thread passes `threadId`, so the render that still sees\n * the previous thread's state after a switch reads no lane at all.\n */\nexport function useDeepAgentConfig({\n threadId,\n ...options\n}: UseDeepAgentConfigOptions = {}): UseDeepAgentConfigResult {\n const sendCommand = useStatewireSendCommand();\n const selectServerConfig = useCallback(\n (state: unknown) => readThreadServerConfig({ state, threadId }),\n [threadId]\n );\n const serverConfig = useStatewireState(selectServerConfig);\n return useDeepAgentConfigController({\n serverConfig,\n sendCommand: (command) => Promise.resolve(sendCommand(command as never)),\n ...options,\n });\n}\n\nexport interface DeepAgentConfigControllerInputs extends UseDeepAgentConfigOptions {\n serverConfig: DeepAgentServerConfig | null;\n sendCommand: (command: ConfigCommand) => Promise<unknown>;\n}\n\n/**\n * The transport-agnostic body of {@link useDeepAgentConfig}, with the reactive\n * inputs injected — so the optimistic-overlay behavior is testable without a\n * statewire session.\n */\nexport function useDeepAgentConfigController({\n serverConfig,\n sendCommand,\n onServerConfig,\n}: DeepAgentConfigControllerInputs): UseDeepAgentConfigResult {\n const [ops, setOps] = useState<readonly PendingOp[]>([]);\n const nextOpIdRef = useRef(1);\n const sendCommandRef = useRef(sendCommand);\n sendCommandRef.current = sendCommand;\n const onServerConfigRef = useRef(onServerConfig);\n onServerConfigRef.current = onServerConfig;\n\n const custom = useMemo(() => foldPendingConfigOps(serverConfig, ops), [serverConfig, ops]);\n\n // Retire ops the lane has caught up with (their transform is now part of\n // the server config itself).\n const serverVersion = serverConfig?.version ?? 0;\n const serverVersionRef = useRef(serverVersion);\n serverVersionRef.current = serverVersion;\n useEffect(() => {\n setOps((current) => {\n const kept = current.filter(\n (op) => op.settledVersion === null || op.settledVersion > serverVersion\n );\n return kept.length === current.length ? current : kept;\n });\n }, [serverVersion]);\n\n const lastNotifiedRef = useRef<DeepAgentServerConfig | null>(null);\n useEffect(() => {\n if (serverConfig === null || serverConfig === lastNotifiedRef.current) return;\n lastNotifiedRef.current = serverConfig;\n onServerConfigRef.current?.({ version: serverConfig.version, custom });\n }, [serverConfig, custom]);\n\n const run = useCallback(\n async (\n command: ConfigCommand,\n apply: PendingOp['apply']\n ): Promise<DeepAgentConfigWriteResult> => {\n const id = nextOpIdRef.current++;\n setOps((current) => [...current, { id, apply, settledVersion: null }]);\n try {\n const raw = (await sendCommandRef.current(command)) as\n | { version?: unknown; custom?: unknown }\n | null\n | undefined;\n const version = typeof raw?.version === 'number' ? raw.version : Number.POSITIVE_INFINITY;\n // The lane echo may have landed before the acknowledgement: retire\n // now if so, otherwise stamp the version for the retire effect.\n setOps((current) =>\n version <= serverVersionRef.current\n ? current.filter((op) => op.id !== id)\n : current.map((op) => (op.id === id ? { ...op, settledVersion: version } : op))\n );\n return {\n version: Number.isFinite(version) ? version : 0,\n custom: isRecord(raw?.custom) ? raw.custom : apply({}),\n };\n } catch (error) {\n setOps((current) => current.filter((op) => op.id !== id));\n throw error;\n }\n },\n []\n );\n\n const patchConfig = useCallback(\n (patch: ThreadConfigPatch) =>\n run({ type: DEEP_AGENT_CONFIG_COMMANDS.patch, patch }, (base) =>\n applyConfigPatch(base, patch)\n ),\n [run]\n );\n const enableTools = useCallback(\n (toolIds: readonly string[]) =>\n run({ type: DEEP_AGENT_CONFIG_COMMANDS.toolsEnable, toolIds: [...toolIds] }, (base) =>\n applyToolsEnable(base, toolIds)\n ),\n [run]\n );\n const disableTools = useCallback(\n (toolIds: readonly string[]) =>\n run({ type: DEEP_AGENT_CONFIG_COMMANDS.toolsDisable, toolIds: [...toolIds] }, (base) =>\n applyToolsDisable(base, toolIds)\n ),\n [run]\n );\n\n return {\n configVersion: serverVersion,\n serverConfig,\n custom,\n hasPendingWrites: ops.some((op) => op.settledVersion === null),\n patchConfig,\n enableTools,\n disableTools,\n };\n}\n"],"names":["statewireRejectionPayload","useState","useRef","useMemo","useEffect","useCallback","DEEP_AGENT_CONFIG_COMMANDS","applyConfigPatch","applyToolsEnable","applyToolsDisable"],"mappings":"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"}
|
|
@@ -18,8 +18,9 @@ const CONFIG_WRITE_FORBIDDEN_CODES = /* @__PURE__ */ new Set([
|
|
|
18
18
|
]);
|
|
19
19
|
function isConfigWriteForbiddenRejection(error) {
|
|
20
20
|
const payload = statewireRejectionPayload(error);
|
|
21
|
+
const detail = payload == null ? void 0 : payload.detail;
|
|
22
|
+
if ((payload == null ? void 0 : payload.status) === 409) return (detail == null ? void 0 : detail.code) === "conversation_archived";
|
|
21
23
|
if ((payload == null ? void 0 : payload.status) !== 403) return false;
|
|
22
|
-
const detail = payload.detail;
|
|
23
24
|
return typeof (detail == null ? void 0 : detail.code) === "string" && CONFIG_WRITE_FORBIDDEN_CODES.has(detail.code);
|
|
24
25
|
}
|
|
25
26
|
function isConfigSyncUnavailableRejection(error) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-deep-agent-config.js","sources":["../../../../agent-runtime/src/use-deep-agent-config.ts"],"sourcesContent":["import { useStatewireSendCommand, useStatewireState } from '@assistant-ui/react-statewire';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport {\n applyConfigPatch,\n applyToolsDisable,\n applyToolsEnable,\n type ThreadConfigCustom,\n type ThreadConfigPatch,\n} from './config-mutations';\nimport { statewireRejectionPayload } from './errors';\nimport { DEEP_AGENT_CONFIG_COMMANDS } from './generated/runconfig-contract';\nimport { readDeepAgentStateThreadId } from './statewire-state';\n\ndeclare module '@assistant-ui/core' {\n namespace Assistant {\n interface Commands {\n /** Set / unset top-level keys of the thread's server config (`null` unsets). */\n configPatch: {\n type: typeof DEEP_AGENT_CONFIG_COMMANDS.patch;\n patch: Record<string, unknown>;\n };\n /** Append ids to the thread's `enabled_tools`. */\n configToolsEnable: {\n type: typeof DEEP_AGENT_CONFIG_COMMANDS.toolsEnable;\n toolIds: string[];\n };\n /** Remove ids from the thread's `enabled_tools`. */\n configToolsDisable: {\n type: typeof DEEP_AGENT_CONFIG_COMMANDS.toolsDisable;\n toolIds: string[];\n };\n }\n }\n}\n\n/**\n * The replicated `state.config` lane of a thread with server-authoritative\n * run config. Absent — and `readServerConfig` returns `null` — while the\n * thread has never been configured (version 0: a brand-new thread, or one\n * created by a client predating the lane).\n */\nexport interface DeepAgentServerConfig {\n /** Monotonic server-incremented version; always >= 1 when the lane exists. */\n version: number;\n /** The thread-config keys every run on this thread uses as its base. */\n custom: ThreadConfigCustom;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n/**\n * Read the server-config lane off a raw statewire state.\n *\n * Identity-preserving (returns the raw `state.config` reference), so it is\n * safe as a `useStatewireState` selector: the reference only changes when the\n * server writes the lane.\n */\nexport function readServerConfig(state: unknown): DeepAgentServerConfig | null {\n const config = (state as { config?: unknown } | null | undefined)?.config;\n if (!isRecord(config)) return null;\n const version = config.version;\n if (typeof version !== 'number' || !Number.isInteger(version) || version <= 0) return null;\n if (!isRecord(config.custom)) return null;\n return config as unknown as DeepAgentServerConfig;\n}\n\n/** The thread's server config version: 0 while the thread has never been configured. */\nexport function readConfigVersion(state: unknown): number {\n return readServerConfig(state)?.version ?? 0;\n}\n\n/**\n * {@link readServerConfig}, but only from a state `threadId`'s own attach\n * produced (`readDeepAgentStateThreadId`); any other state reads as\n * unconfigured. With no `threadId` it is {@link readServerConfig} unchanged.\n *\n * Identity-preserving like {@link readServerConfig}, so it is safe as a\n * `useStatewireState` selector.\n */\nexport function readThreadServerConfig({\n state,\n threadId,\n}: {\n state: unknown;\n threadId: string | undefined;\n}): DeepAgentServerConfig | null {\n if (threadId !== undefined && readDeepAgentStateThreadId(state) !== threadId) return null;\n return readServerConfig(state);\n}\n\n/**\n * The 403 rejection codes a view-only collaborator's `config/*` write can\n * come back with: `config_write_forbidden` from the config host's own gate,\n * or `thread_write_forbidden` from the host-wide mutation gate that runs\n * ahead of it and refuses every thread-mutating command on a view grant.\n */\nconst CONFIG_WRITE_FORBIDDEN_CODES: ReadonlySet<string> = new Set([\n 'config_write_forbidden',\n 'thread_write_forbidden',\n]);\n\n/**\n * Whether a rejected `config/*` command was refused because the submitter\n * only has view access to the session. The write is not retriable by that\n * user; hosts disable their config controls.\n */\nexport function isConfigWriteForbiddenRejection(error: unknown): boolean {\n const payload = statewireRejectionPayload(error);\n if (payload?.status !== 403) return false;\n const detail = payload.detail as { code?: unknown } | undefined;\n return typeof detail?.code === 'string' && CONFIG_WRITE_FORBIDDEN_CODES.has(detail.code);\n}\n\n/**\n * Whether a rejected `config/*` command was refused because the thread's\n * workspace cannot hold server config at all (`config_sync_unavailable`, a\n * regionally-pinned workspace). No write will ever succeed on the thread;\n * hosts stop issuing them and send their full config per run instead.\n */\nexport function isConfigSyncUnavailableRejection(error: unknown): boolean {\n const payload = statewireRejectionPayload(error);\n if (payload?.status !== 400) return false;\n const detail = payload.detail as { code?: unknown } | undefined;\n return detail?.code === 'config_sync_unavailable';\n}\n\n/** The settled result of a `config/*` command: the new server config. */\nexport interface DeepAgentConfigWriteResult {\n version: number;\n custom: ThreadConfigCustom;\n}\n\n/** An observed server-config version, delivered to the host callback. */\nexport interface DeepAgentServerConfigEvent {\n version: number;\n /** The server config with this client's still-pending writes folded over. */\n custom: ThreadConfigCustom;\n}\n\ninterface PendingOp {\n id: number;\n apply: (custom: ThreadConfigCustom) => ThreadConfigCustom;\n /** The version the server assigned this op; retired once the lane reaches it. */\n settledVersion: number | null;\n}\n\n/**\n * Fold the pending optimistic ops over the server config — the effective\n * config a client shows and sends against. Pure, for the tests.\n */\nexport function foldPendingConfigOps(\n serverConfig: DeepAgentServerConfig | null,\n ops: readonly Pick<PendingOp, 'apply' | 'settledVersion'>[]\n): ThreadConfigCustom {\n const version = serverConfig?.version ?? 0;\n let custom: ThreadConfigCustom = serverConfig?.custom ?? {};\n for (const op of ops) {\n if (op.settledVersion !== null && op.settledVersion <= version) continue;\n custom = op.apply(custom);\n }\n return custom;\n}\n\nexport interface UseDeepAgentConfigOptions {\n /**\n * Fired whenever the server writes a new version of the lane (another\n * device, the seed, or the echo of our own command), with this client's\n * still-pending writes folded over — hosts mirror it into their local\n * store; nothing is ever clobbered because pending edits stay on top.\n */\n onServerConfig?: (event: DeepAgentServerConfigEvent) => void;\n /**\n * The thread this binding serves. When set, the lane is read only from a\n * state that thread's own attach produced ({@link readThreadServerConfig}):\n * for the first render after a thread switch the client still projects the\n * previous thread's state, and adopting its lane would report the previous\n * thread's config and version as this thread's.\n */\n threadId?: string;\n}\n\nexport interface UseDeepAgentConfigResult {\n /** Reactive server config version (0 while the thread has never been configured). */\n configVersion: number;\n /** Reactive server-config lane, or `null` at version 0. */\n serverConfig: DeepAgentServerConfig | null;\n /**\n * The effective thread config: the server lane with this client's pending\n * writes folded over. `{}` at version 0 with nothing pending.\n */\n custom: ThreadConfigCustom;\n /** `true` while at least one `config/*` command is unacknowledged by the server. */\n hasPendingWrites: boolean;\n /** `config/patch` — set / unset top-level keys; `null` unsets. */\n patchConfig: (patch: ThreadConfigPatch) => Promise<DeepAgentConfigWriteResult>;\n /** `config/tools/enable`. */\n enableTools: (toolIds: readonly string[]) => Promise<DeepAgentConfigWriteResult>;\n /** `config/tools/disable`. */\n disableTools: (toolIds: readonly string[]) => Promise<DeepAgentConfigWriteResult>;\n}\n\ntype ConfigCommand =\n | { type: typeof DEEP_AGENT_CONFIG_COMMANDS.patch; patch: Record<string, unknown> }\n | { type: typeof DEEP_AGENT_CONFIG_COMMANDS.toolsEnable; toolIds: string[] }\n | { type: typeof DEEP_AGENT_CONFIG_COMMANDS.toolsDisable; toolIds: string[] };\n\n/**\n * The server-authoritative thread config of a deep-agent thread.\n *\n * Every capability a run depends on — model, agent binding, enabled tools,\n * sandbox, environment, … — lives on the thread's server config lane and is\n * changed only through the `config/*` commands. Sends carry per-send keys\n * only (`pickPerSendCustom`). The commands are applied optimistically: each\n * one folds its transform over the server config the moment it is issued\n * (`custom`), and retires once the lane reaches the version the server\n * assigned it; a rejection drops it, so the UI snaps back to the truth.\n *\n * Must be mounted under the thread's statewire context (an `AuiProvider`\n * holding `useDeepAgentThread`'s element), keyed by the thread identity so a\n * thread switch remounts it with an empty pending set. A binding that persists\n * what it reads per thread passes `threadId`, so the render that still sees\n * the previous thread's state after a switch reads no lane at all.\n */\nexport function useDeepAgentConfig({\n threadId,\n ...options\n}: UseDeepAgentConfigOptions = {}): UseDeepAgentConfigResult {\n const sendCommand = useStatewireSendCommand();\n const selectServerConfig = useCallback(\n (state: unknown) => readThreadServerConfig({ state, threadId }),\n [threadId]\n );\n const serverConfig = useStatewireState(selectServerConfig);\n return useDeepAgentConfigController({\n serverConfig,\n sendCommand: (command) => Promise.resolve(sendCommand(command as never)),\n ...options,\n });\n}\n\nexport interface DeepAgentConfigControllerInputs extends UseDeepAgentConfigOptions {\n serverConfig: DeepAgentServerConfig | null;\n sendCommand: (command: ConfigCommand) => Promise<unknown>;\n}\n\n/**\n * The transport-agnostic body of {@link useDeepAgentConfig}, with the reactive\n * inputs injected — so the optimistic-overlay behavior is testable without a\n * statewire session.\n */\nexport function useDeepAgentConfigController({\n serverConfig,\n sendCommand,\n onServerConfig,\n}: DeepAgentConfigControllerInputs): UseDeepAgentConfigResult {\n const [ops, setOps] = useState<readonly PendingOp[]>([]);\n const nextOpIdRef = useRef(1);\n const sendCommandRef = useRef(sendCommand);\n sendCommandRef.current = sendCommand;\n const onServerConfigRef = useRef(onServerConfig);\n onServerConfigRef.current = onServerConfig;\n\n const custom = useMemo(() => foldPendingConfigOps(serverConfig, ops), [serverConfig, ops]);\n\n // Retire ops the lane has caught up with (their transform is now part of\n // the server config itself).\n const serverVersion = serverConfig?.version ?? 0;\n const serverVersionRef = useRef(serverVersion);\n serverVersionRef.current = serverVersion;\n useEffect(() => {\n setOps((current) => {\n const kept = current.filter(\n (op) => op.settledVersion === null || op.settledVersion > serverVersion\n );\n return kept.length === current.length ? current : kept;\n });\n }, [serverVersion]);\n\n const lastNotifiedRef = useRef<DeepAgentServerConfig | null>(null);\n useEffect(() => {\n if (serverConfig === null || serverConfig === lastNotifiedRef.current) return;\n lastNotifiedRef.current = serverConfig;\n onServerConfigRef.current?.({ version: serverConfig.version, custom });\n }, [serverConfig, custom]);\n\n const run = useCallback(\n async (\n command: ConfigCommand,\n apply: PendingOp['apply']\n ): Promise<DeepAgentConfigWriteResult> => {\n const id = nextOpIdRef.current++;\n setOps((current) => [...current, { id, apply, settledVersion: null }]);\n try {\n const raw = (await sendCommandRef.current(command)) as\n | { version?: unknown; custom?: unknown }\n | null\n | undefined;\n const version = typeof raw?.version === 'number' ? raw.version : Number.POSITIVE_INFINITY;\n // The lane echo may have landed before the acknowledgement: retire\n // now if so, otherwise stamp the version for the retire effect.\n setOps((current) =>\n version <= serverVersionRef.current\n ? current.filter((op) => op.id !== id)\n : current.map((op) => (op.id === id ? { ...op, settledVersion: version } : op))\n );\n return {\n version: Number.isFinite(version) ? version : 0,\n custom: isRecord(raw?.custom) ? raw.custom : apply({}),\n };\n } catch (error) {\n setOps((current) => current.filter((op) => op.id !== id));\n throw error;\n }\n },\n []\n );\n\n const patchConfig = useCallback(\n (patch: ThreadConfigPatch) =>\n run({ type: DEEP_AGENT_CONFIG_COMMANDS.patch, patch }, (base) =>\n applyConfigPatch(base, patch)\n ),\n [run]\n );\n const enableTools = useCallback(\n (toolIds: readonly string[]) =>\n run({ type: DEEP_AGENT_CONFIG_COMMANDS.toolsEnable, toolIds: [...toolIds] }, (base) =>\n applyToolsEnable(base, toolIds)\n ),\n [run]\n );\n const disableTools = useCallback(\n (toolIds: readonly string[]) =>\n run({ type: DEEP_AGENT_CONFIG_COMMANDS.toolsDisable, toolIds: [...toolIds] }, (base) =>\n applyToolsDisable(base, toolIds)\n ),\n [run]\n );\n\n return {\n configVersion: serverVersion,\n serverConfig,\n custom,\n hasPendingWrites: ops.some((op) => op.settledVersion === null),\n patchConfig,\n enableTools,\n disableTools,\n };\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-deep-agent-config.js","sources":["../../../../agent-runtime/src/use-deep-agent-config.ts"],"sourcesContent":["import { useStatewireSendCommand, useStatewireState } from '@assistant-ui/react-statewire';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport {\n applyConfigPatch,\n applyToolsDisable,\n applyToolsEnable,\n type ThreadConfigCustom,\n type ThreadConfigPatch,\n} from './config-mutations';\nimport { statewireRejectionPayload } from './errors';\nimport { DEEP_AGENT_CONFIG_COMMANDS } from './generated/runconfig-contract';\nimport { readDeepAgentStateThreadId } from './statewire-state';\n\ndeclare module '@assistant-ui/core' {\n namespace Assistant {\n interface Commands {\n /** Set / unset top-level keys of the thread's server config (`null` unsets). */\n configPatch: {\n type: typeof DEEP_AGENT_CONFIG_COMMANDS.patch;\n patch: Record<string, unknown>;\n };\n /** Append ids to the thread's `enabled_tools`. */\n configToolsEnable: {\n type: typeof DEEP_AGENT_CONFIG_COMMANDS.toolsEnable;\n toolIds: string[];\n };\n /** Remove ids from the thread's `enabled_tools`. */\n configToolsDisable: {\n type: typeof DEEP_AGENT_CONFIG_COMMANDS.toolsDisable;\n toolIds: string[];\n };\n }\n }\n}\n\n/**\n * The replicated `state.config` lane of a thread with server-authoritative\n * run config. Absent — and `readServerConfig` returns `null` — while the\n * thread has never been configured (version 0: a brand-new thread, or one\n * created by a client predating the lane).\n */\nexport interface DeepAgentServerConfig {\n /** Monotonic server-incremented version; always >= 1 when the lane exists. */\n version: number;\n /** The thread-config keys every run on this thread uses as its base. */\n custom: ThreadConfigCustom;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n/**\n * Read the server-config lane off a raw statewire state.\n *\n * Identity-preserving (returns the raw `state.config` reference), so it is\n * safe as a `useStatewireState` selector: the reference only changes when the\n * server writes the lane.\n */\nexport function readServerConfig(state: unknown): DeepAgentServerConfig | null {\n const config = (state as { config?: unknown } | null | undefined)?.config;\n if (!isRecord(config)) return null;\n const version = config.version;\n if (typeof version !== 'number' || !Number.isInteger(version) || version <= 0) return null;\n if (!isRecord(config.custom)) return null;\n return config as unknown as DeepAgentServerConfig;\n}\n\n/** The thread's server config version: 0 while the thread has never been configured. */\nexport function readConfigVersion(state: unknown): number {\n return readServerConfig(state)?.version ?? 0;\n}\n\n/**\n * {@link readServerConfig}, but only from a state `threadId`'s own attach\n * produced (`readDeepAgentStateThreadId`); any other state reads as\n * unconfigured. With no `threadId` it is {@link readServerConfig} unchanged.\n *\n * Identity-preserving like {@link readServerConfig}, so it is safe as a\n * `useStatewireState` selector.\n */\nexport function readThreadServerConfig({\n state,\n threadId,\n}: {\n state: unknown;\n threadId: string | undefined;\n}): DeepAgentServerConfig | null {\n if (threadId !== undefined && readDeepAgentStateThreadId(state) !== threadId) return null;\n return readServerConfig(state);\n}\n\n/**\n * The 403 rejection codes a view-only collaborator's `config/*` write can\n * come back with: `config_write_forbidden` from the config host's own gate,\n * or `thread_write_forbidden` from the host-wide mutation gate that runs\n * ahead of it and refuses every thread-mutating command on a view grant.\n */\nconst CONFIG_WRITE_FORBIDDEN_CODES: ReadonlySet<string> = new Set([\n 'config_write_forbidden',\n 'thread_write_forbidden',\n]);\n\n/**\n * Whether a rejected `config/*` command was refused because the submitter\n * only has view access to the session, or because the conversation is\n * archived (`conversation_archived`, a 409: read-only until it is restored).\n * The write is not retriable by that user; hosts disable their config\n * controls.\n */\nexport function isConfigWriteForbiddenRejection(error: unknown): boolean {\n const payload = statewireRejectionPayload(error);\n const detail = payload?.detail as { code?: unknown } | undefined;\n if (payload?.status === 409) return detail?.code === 'conversation_archived';\n if (payload?.status !== 403) return false;\n return typeof detail?.code === 'string' && CONFIG_WRITE_FORBIDDEN_CODES.has(detail.code);\n}\n\n/**\n * Whether a rejected `config/*` command was refused because the thread's\n * workspace cannot hold server config at all (`config_sync_unavailable`, a\n * regionally-pinned workspace). No write will ever succeed on the thread;\n * hosts stop issuing them and send their full config per run instead.\n */\nexport function isConfigSyncUnavailableRejection(error: unknown): boolean {\n const payload = statewireRejectionPayload(error);\n if (payload?.status !== 400) return false;\n const detail = payload.detail as { code?: unknown } | undefined;\n return detail?.code === 'config_sync_unavailable';\n}\n\n/** The settled result of a `config/*` command: the new server config. */\nexport interface DeepAgentConfigWriteResult {\n version: number;\n custom: ThreadConfigCustom;\n}\n\n/** An observed server-config version, delivered to the host callback. */\nexport interface DeepAgentServerConfigEvent {\n version: number;\n /** The server config with this client's still-pending writes folded over. */\n custom: ThreadConfigCustom;\n}\n\ninterface PendingOp {\n id: number;\n apply: (custom: ThreadConfigCustom) => ThreadConfigCustom;\n /** The version the server assigned this op; retired once the lane reaches it. */\n settledVersion: number | null;\n}\n\n/**\n * Fold the pending optimistic ops over the server config — the effective\n * config a client shows and sends against. Pure, for the tests.\n */\nexport function foldPendingConfigOps(\n serverConfig: DeepAgentServerConfig | null,\n ops: readonly Pick<PendingOp, 'apply' | 'settledVersion'>[]\n): ThreadConfigCustom {\n const version = serverConfig?.version ?? 0;\n let custom: ThreadConfigCustom = serverConfig?.custom ?? {};\n for (const op of ops) {\n if (op.settledVersion !== null && op.settledVersion <= version) continue;\n custom = op.apply(custom);\n }\n return custom;\n}\n\nexport interface UseDeepAgentConfigOptions {\n /**\n * Fired whenever the server writes a new version of the lane (another\n * device, the seed, or the echo of our own command), with this client's\n * still-pending writes folded over — hosts mirror it into their local\n * store; nothing is ever clobbered because pending edits stay on top.\n */\n onServerConfig?: (event: DeepAgentServerConfigEvent) => void;\n /**\n * The thread this binding serves. When set, the lane is read only from a\n * state that thread's own attach produced ({@link readThreadServerConfig}):\n * for the first render after a thread switch the client still projects the\n * previous thread's state, and adopting its lane would report the previous\n * thread's config and version as this thread's.\n */\n threadId?: string;\n}\n\nexport interface UseDeepAgentConfigResult {\n /** Reactive server config version (0 while the thread has never been configured). */\n configVersion: number;\n /** Reactive server-config lane, or `null` at version 0. */\n serverConfig: DeepAgentServerConfig | null;\n /**\n * The effective thread config: the server lane with this client's pending\n * writes folded over. `{}` at version 0 with nothing pending.\n */\n custom: ThreadConfigCustom;\n /** `true` while at least one `config/*` command is unacknowledged by the server. */\n hasPendingWrites: boolean;\n /** `config/patch` — set / unset top-level keys; `null` unsets. */\n patchConfig: (patch: ThreadConfigPatch) => Promise<DeepAgentConfigWriteResult>;\n /** `config/tools/enable`. */\n enableTools: (toolIds: readonly string[]) => Promise<DeepAgentConfigWriteResult>;\n /** `config/tools/disable`. */\n disableTools: (toolIds: readonly string[]) => Promise<DeepAgentConfigWriteResult>;\n}\n\ntype ConfigCommand =\n | { type: typeof DEEP_AGENT_CONFIG_COMMANDS.patch; patch: Record<string, unknown> }\n | { type: typeof DEEP_AGENT_CONFIG_COMMANDS.toolsEnable; toolIds: string[] }\n | { type: typeof DEEP_AGENT_CONFIG_COMMANDS.toolsDisable; toolIds: string[] };\n\n/**\n * The server-authoritative thread config of a deep-agent thread.\n *\n * Every capability a run depends on — model, agent binding, enabled tools,\n * sandbox, environment, … — lives on the thread's server config lane and is\n * changed only through the `config/*` commands. Sends carry per-send keys\n * only (`pickPerSendCustom`). The commands are applied optimistically: each\n * one folds its transform over the server config the moment it is issued\n * (`custom`), and retires once the lane reaches the version the server\n * assigned it; a rejection drops it, so the UI snaps back to the truth.\n *\n * Must be mounted under the thread's statewire context (an `AuiProvider`\n * holding `useDeepAgentThread`'s element), keyed by the thread identity so a\n * thread switch remounts it with an empty pending set. A binding that persists\n * what it reads per thread passes `threadId`, so the render that still sees\n * the previous thread's state after a switch reads no lane at all.\n */\nexport function useDeepAgentConfig({\n threadId,\n ...options\n}: UseDeepAgentConfigOptions = {}): UseDeepAgentConfigResult {\n const sendCommand = useStatewireSendCommand();\n const selectServerConfig = useCallback(\n (state: unknown) => readThreadServerConfig({ state, threadId }),\n [threadId]\n );\n const serverConfig = useStatewireState(selectServerConfig);\n return useDeepAgentConfigController({\n serverConfig,\n sendCommand: (command) => Promise.resolve(sendCommand(command as never)),\n ...options,\n });\n}\n\nexport interface DeepAgentConfigControllerInputs extends UseDeepAgentConfigOptions {\n serverConfig: DeepAgentServerConfig | null;\n sendCommand: (command: ConfigCommand) => Promise<unknown>;\n}\n\n/**\n * The transport-agnostic body of {@link useDeepAgentConfig}, with the reactive\n * inputs injected — so the optimistic-overlay behavior is testable without a\n * statewire session.\n */\nexport function useDeepAgentConfigController({\n serverConfig,\n sendCommand,\n onServerConfig,\n}: DeepAgentConfigControllerInputs): UseDeepAgentConfigResult {\n const [ops, setOps] = useState<readonly PendingOp[]>([]);\n const nextOpIdRef = useRef(1);\n const sendCommandRef = useRef(sendCommand);\n sendCommandRef.current = sendCommand;\n const onServerConfigRef = useRef(onServerConfig);\n onServerConfigRef.current = onServerConfig;\n\n const custom = useMemo(() => foldPendingConfigOps(serverConfig, ops), [serverConfig, ops]);\n\n // Retire ops the lane has caught up with (their transform is now part of\n // the server config itself).\n const serverVersion = serverConfig?.version ?? 0;\n const serverVersionRef = useRef(serverVersion);\n serverVersionRef.current = serverVersion;\n useEffect(() => {\n setOps((current) => {\n const kept = current.filter(\n (op) => op.settledVersion === null || op.settledVersion > serverVersion\n );\n return kept.length === current.length ? current : kept;\n });\n }, [serverVersion]);\n\n const lastNotifiedRef = useRef<DeepAgentServerConfig | null>(null);\n useEffect(() => {\n if (serverConfig === null || serverConfig === lastNotifiedRef.current) return;\n lastNotifiedRef.current = serverConfig;\n onServerConfigRef.current?.({ version: serverConfig.version, custom });\n }, [serverConfig, custom]);\n\n const run = useCallback(\n async (\n command: ConfigCommand,\n apply: PendingOp['apply']\n ): Promise<DeepAgentConfigWriteResult> => {\n const id = nextOpIdRef.current++;\n setOps((current) => [...current, { id, apply, settledVersion: null }]);\n try {\n const raw = (await sendCommandRef.current(command)) as\n | { version?: unknown; custom?: unknown }\n | null\n | undefined;\n const version = typeof raw?.version === 'number' ? raw.version : Number.POSITIVE_INFINITY;\n // The lane echo may have landed before the acknowledgement: retire\n // now if so, otherwise stamp the version for the retire effect.\n setOps((current) =>\n version <= serverVersionRef.current\n ? current.filter((op) => op.id !== id)\n : current.map((op) => (op.id === id ? { ...op, settledVersion: version } : op))\n );\n return {\n version: Number.isFinite(version) ? version : 0,\n custom: isRecord(raw?.custom) ? raw.custom : apply({}),\n };\n } catch (error) {\n setOps((current) => current.filter((op) => op.id !== id));\n throw error;\n }\n },\n []\n );\n\n const patchConfig = useCallback(\n (patch: ThreadConfigPatch) =>\n run({ type: DEEP_AGENT_CONFIG_COMMANDS.patch, patch }, (base) =>\n applyConfigPatch(base, patch)\n ),\n [run]\n );\n const enableTools = useCallback(\n (toolIds: readonly string[]) =>\n run({ type: DEEP_AGENT_CONFIG_COMMANDS.toolsEnable, toolIds: [...toolIds] }, (base) =>\n applyToolsEnable(base, toolIds)\n ),\n [run]\n );\n const disableTools = useCallback(\n (toolIds: readonly string[]) =>\n run({ type: DEEP_AGENT_CONFIG_COMMANDS.toolsDisable, toolIds: [...toolIds] }, (base) =>\n applyToolsDisable(base, toolIds)\n ),\n [run]\n );\n\n return {\n configVersion: serverVersion,\n serverConfig,\n custom,\n hasPendingWrites: ops.some((op) => op.settledVersion === null),\n patchConfig,\n enableTools,\n disableTools,\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,129 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const jsxRuntime = require("react/jsx-runtime");
|
|
4
|
+
const reactQuery = require("@tanstack/react-query");
|
|
4
5
|
const react = require("react");
|
|
6
|
+
const athenaDiagnostics = require("../diagnostics/athena-diagnostics.cjs");
|
|
7
|
+
const errors = require("../diagnostics/errors.cjs");
|
|
8
|
+
const auth = require("../runtime/auth.cjs");
|
|
5
9
|
const AthenaAuthContext = require("./AthenaAuthContext.cjs");
|
|
10
|
+
const SSO_USERINFO_RETRY_POLICY = {
|
|
11
|
+
...auth.DEFAULT_MINT_RETRY_POLICY,
|
|
12
|
+
timeoutMs: 1e4
|
|
13
|
+
};
|
|
14
|
+
const abortError = () => new DOMException("The session read was cancelled.", "AbortError");
|
|
15
|
+
async function readUserInfoOnce({
|
|
16
|
+
url,
|
|
17
|
+
signal,
|
|
18
|
+
timeoutMs
|
|
19
|
+
}) {
|
|
20
|
+
if (signal.aborted) throw abortError();
|
|
21
|
+
const request = new AbortController();
|
|
22
|
+
const cancel = () => request.abort();
|
|
23
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
24
|
+
let timedOut = false;
|
|
25
|
+
const timer = setTimeout(() => {
|
|
26
|
+
timedOut = true;
|
|
27
|
+
request.abort();
|
|
28
|
+
}, timeoutMs);
|
|
29
|
+
try {
|
|
30
|
+
const response = await fetch(url, { credentials: "include", signal: request.signal });
|
|
31
|
+
if (response.status === 401 || response.status === 403) return { kind: "unauthenticated" };
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
return auth.isRetryableMintStatus(response.status) ? { kind: "transient", status: response.status, reason: "status" } : { kind: "failed", status: response.status };
|
|
34
|
+
}
|
|
35
|
+
let data;
|
|
36
|
+
try {
|
|
37
|
+
data = await response.json();
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (signal.aborted) throw abortError();
|
|
40
|
+
return timedOut ? { kind: "transient", status: null, reason: "timeout", cause: error } : { kind: "failed", status: response.status };
|
|
41
|
+
}
|
|
42
|
+
return isValidSSOUserInfo(data) ? { kind: "session", data } : { kind: "invalid_response" };
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (signal.aborted) throw abortError();
|
|
45
|
+
return { kind: "transient", status: null, reason: timedOut ? "timeout" : "network", cause: error };
|
|
46
|
+
} finally {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
signal.removeEventListener("abort", cancel);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function readSession({
|
|
52
|
+
url,
|
|
53
|
+
signal,
|
|
54
|
+
policy
|
|
55
|
+
}) {
|
|
56
|
+
const maxAttempts = Math.max(1, Math.floor(policy.maxAttempts));
|
|
57
|
+
for (let attempts = 1; ; attempts += 1) {
|
|
58
|
+
const attempt = await readUserInfoOnce({ url, signal, timeoutMs: policy.timeoutMs });
|
|
59
|
+
if (attempt.kind === "session") return { kind: "session", data: attempt.data };
|
|
60
|
+
if (attempt.kind === "unauthenticated") return { kind: "no_session", reason: "unauthenticated" };
|
|
61
|
+
if (attempt.kind === "invalid_response") {
|
|
62
|
+
console.error(
|
|
63
|
+
'[AthenaSDK] Invalid SSO userinfo response: expected "user.user_id", "user.email", "orgMemberInfos", and "accessToken"'
|
|
64
|
+
);
|
|
65
|
+
return { kind: "no_session", reason: "invalid_response" };
|
|
66
|
+
}
|
|
67
|
+
if (attempt.kind === "transient" && attempts < maxAttempts) {
|
|
68
|
+
const delayMs = auth.getMintRetryDelay({ retry: attempts, policy });
|
|
69
|
+
athenaDiagnostics.athenaDiagnostics.emit("auth.sso.userinfo.retry", {
|
|
70
|
+
level: "warn",
|
|
71
|
+
data: {
|
|
72
|
+
status: attempt.status,
|
|
73
|
+
reason: attempt.reason,
|
|
74
|
+
attempt: attempts,
|
|
75
|
+
max_attempts: maxAttempts,
|
|
76
|
+
delay_ms: delayMs
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
await waitBeforeRetry({ delayMs, signal });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
throw sessionReadError({ url, attempt, attempts });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function waitBeforeRetry({ delayMs, signal }) {
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
if (signal.aborted) {
|
|
88
|
+
reject(abortError());
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const onAbort = () => {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
reject(abortError());
|
|
94
|
+
};
|
|
95
|
+
const timer = setTimeout(() => {
|
|
96
|
+
signal.removeEventListener("abort", onAbort);
|
|
97
|
+
resolve();
|
|
98
|
+
}, delayMs);
|
|
99
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function sessionReadError({
|
|
103
|
+
url,
|
|
104
|
+
attempt,
|
|
105
|
+
attempts
|
|
106
|
+
}) {
|
|
107
|
+
if (attempt.kind === "failed") {
|
|
108
|
+
const unreadable = attempt.status >= 200 && attempt.status < 300;
|
|
109
|
+
return new errors.AthenaSdkError({
|
|
110
|
+
code: "auth_sso_failed",
|
|
111
|
+
message: unreadable ? `The SSO session read (${url}) answered HTTP ${attempt.status} without a JSON body.` : `The SSO session read (${url}) answered HTTP ${attempt.status}.`,
|
|
112
|
+
hint: "Check that ssoBaseUrl / ssoUserInfoUrl point at the SSO userinfo endpoint.",
|
|
113
|
+
status: attempt.status,
|
|
114
|
+
context: { attempts, reason: unreadable ? "invalid_body" : "status" }
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const answer = attempt.status !== null ? `HTTP ${attempt.status}` : attempt.reason === "timeout" ? "a timeout" : "a network error";
|
|
118
|
+
return new errors.AthenaSdkError({
|
|
119
|
+
code: "auth_sso_failed",
|
|
120
|
+
message: `The SSO session read (${url}) failed ${attempts} time${attempts === 1 ? "" : "s"} (last: ${answer}).`,
|
|
121
|
+
hint: "The sign-in service did not answer; the person may still be signed in. Offer a retry (displayOnError receives retry).",
|
|
122
|
+
...attempt.status !== null && { status: attempt.status },
|
|
123
|
+
...attempt.cause !== void 0 && { cause: attempt.cause },
|
|
124
|
+
context: { attempts, reason: attempt.reason }
|
|
125
|
+
});
|
|
126
|
+
}
|
|
6
127
|
const DEFAULT_SSO_PATHS = {
|
|
7
128
|
userInfo: "/api/sso/userinfo",
|
|
8
129
|
login: "/api/sso/initiate",
|
|
@@ -54,13 +175,15 @@ function AthenaSSOProvider({
|
|
|
54
175
|
ssoLogoutUrl,
|
|
55
176
|
displayWhileLoading,
|
|
56
177
|
displayOnError,
|
|
57
|
-
|
|
178
|
+
onError,
|
|
179
|
+
revalidateOnWindowFocus = false,
|
|
180
|
+
retryPolicy = SSO_USERINFO_RETRY_POLICY
|
|
58
181
|
}) {
|
|
59
|
-
const [
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
182
|
+
const [queryClient] = react.useState(() => new reactQuery.QueryClient());
|
|
183
|
+
const retryPolicyRef = react.useRef(retryPolicy);
|
|
184
|
+
retryPolicyRef.current = retryPolicy;
|
|
185
|
+
const onErrorRef = react.useRef(onError);
|
|
186
|
+
onErrorRef.current = onError;
|
|
64
187
|
const ssoUrls = react.useMemo(
|
|
65
188
|
() => ({
|
|
66
189
|
userInfo: resolveSSOUrl({
|
|
@@ -81,101 +204,75 @@ function AthenaSSOProvider({
|
|
|
81
204
|
}),
|
|
82
205
|
[ssoBaseUrl, ssoUserInfoUrl, ssoLoginUrl, ssoLogoutUrl]
|
|
83
206
|
);
|
|
84
|
-
const
|
|
85
|
-
|
|
207
|
+
const queryKey = react.useMemo(() => ["athena-sdk", "sso-session", ssoUrls.userInfo], [ssoUrls.userInfo]);
|
|
208
|
+
const queryFn = react.useCallback(
|
|
209
|
+
async ({ signal }) => {
|
|
86
210
|
var _a;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
const response = await fetch(ssoUrls.userInfo, {
|
|
98
|
-
credentials: "include",
|
|
99
|
-
signal: abortController.signal
|
|
100
|
-
});
|
|
101
|
-
if (!response.ok) {
|
|
102
|
-
if (response.status === 401 || response.status === 403) {
|
|
103
|
-
setSSOData(null);
|
|
104
|
-
setError("unauthenticated");
|
|
105
|
-
} else {
|
|
106
|
-
setError("request_failed");
|
|
107
|
-
}
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
const data = await response.json();
|
|
111
|
-
if (!isValidSSOUserInfo(data)) {
|
|
112
|
-
console.error(
|
|
113
|
-
'[AthenaSDK] Invalid SSO userinfo response: expected "user.user_id", "user.email", "orgMemberInfos", and "accessToken"'
|
|
114
|
-
);
|
|
115
|
-
setSSOData(null);
|
|
116
|
-
setError("invalid_response");
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
setSSOData((prev) => {
|
|
120
|
-
if (prev && prev.accessToken === data.accessToken && prev.user.user_id === data.user.user_id) {
|
|
121
|
-
return prev;
|
|
122
|
-
}
|
|
123
|
-
return data;
|
|
124
|
-
});
|
|
125
|
-
setError(null);
|
|
126
|
-
return data;
|
|
127
|
-
} catch (fetchError) {
|
|
128
|
-
if (fetchError instanceof DOMException && fetchError.name === "AbortError") {
|
|
129
|
-
return activeRequestRef.current !== abortController ? activeReadRef.current : null;
|
|
130
|
-
}
|
|
131
|
-
setError("request_failed");
|
|
132
|
-
return null;
|
|
133
|
-
} finally {
|
|
134
|
-
if (activeRequestRef.current === abortController) {
|
|
135
|
-
activeRequestRef.current = null;
|
|
136
|
-
setLoading(false);
|
|
211
|
+
try {
|
|
212
|
+
return await readSession({ url: ssoUrls.userInfo, signal, policy: retryPolicyRef.current });
|
|
213
|
+
} catch (readError) {
|
|
214
|
+
if (readError instanceof errors.AthenaSdkError) {
|
|
215
|
+
athenaDiagnostics.athenaDiagnostics.emit("auth.sso.userinfo.error", { error: readError });
|
|
216
|
+
try {
|
|
217
|
+
(_a = onErrorRef.current) == null ? void 0 : _a.call(onErrorRef, readError);
|
|
218
|
+
} catch (callbackError) {
|
|
219
|
+
console.error("[AthenaSDK] AthenaSSOProvider onError threw", callbackError);
|
|
137
220
|
}
|
|
138
221
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return read;
|
|
222
|
+
throw readError;
|
|
223
|
+
}
|
|
142
224
|
},
|
|
143
225
|
[ssoUrls.userInfo]
|
|
144
226
|
);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
227
|
+
const query = reactQuery.useQuery(
|
|
228
|
+
{
|
|
229
|
+
queryKey,
|
|
230
|
+
queryFn,
|
|
231
|
+
// `readSession` retries a transient failure itself, with the bridge's
|
|
232
|
+
// policy; the query never retries on top of it, and reads while the
|
|
233
|
+
// browser reports itself offline too, so that policy decides.
|
|
234
|
+
retry: false,
|
|
235
|
+
networkMode: "always",
|
|
236
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
237
|
+
refetchOnWindowFocus: false,
|
|
238
|
+
refetchOnReconnect: false
|
|
239
|
+
},
|
|
240
|
+
queryClient
|
|
241
|
+
);
|
|
242
|
+
const answer = query.data;
|
|
243
|
+
const session = (answer == null ? void 0 : answer.kind) === "session" ? answer.data : null;
|
|
244
|
+
const loading = query.isPending;
|
|
245
|
+
const reason = session ? null : (answer == null ? void 0 : answer.kind) === "no_session" ? answer.reason : query.isError ? "request_failed" : null;
|
|
246
|
+
const signedOut = (answer == null ? void 0 : answer.kind) === "no_session" && answer.signedOut === true;
|
|
152
247
|
react.useEffect(() => {
|
|
153
248
|
if (!revalidateOnWindowFocus || typeof window === "undefined") {
|
|
154
249
|
return;
|
|
155
250
|
}
|
|
156
|
-
const
|
|
157
|
-
void
|
|
251
|
+
const revalidate = () => {
|
|
252
|
+
void queryClient.refetchQueries({ queryKey }, { cancelRefetch: false });
|
|
158
253
|
};
|
|
159
254
|
const handleVisibilityChange = () => {
|
|
160
255
|
if (document.visibilityState === "visible") {
|
|
161
|
-
|
|
256
|
+
revalidate();
|
|
162
257
|
}
|
|
163
258
|
};
|
|
164
|
-
window.addEventListener("focus",
|
|
259
|
+
window.addEventListener("focus", revalidate);
|
|
165
260
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
166
261
|
return () => {
|
|
167
|
-
window.removeEventListener("focus",
|
|
262
|
+
window.removeEventListener("focus", revalidate);
|
|
168
263
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
169
264
|
};
|
|
170
|
-
}, [revalidateOnWindowFocus,
|
|
265
|
+
}, [revalidateOnWindowFocus, queryClient, queryKey]);
|
|
171
266
|
react.useEffect(() => {
|
|
172
|
-
if (!loading &&
|
|
267
|
+
if (!loading && reason === "unauthenticated" && !signedOut && !displayOnError && typeof window !== "undefined") {
|
|
173
268
|
window.location.assign(ssoUrls.login);
|
|
174
269
|
}
|
|
175
|
-
}, [loading,
|
|
270
|
+
}, [loading, reason, signedOut, displayOnError, ssoUrls.login]);
|
|
271
|
+
const retry = react.useCallback(() => {
|
|
272
|
+
void queryClient.resetQueries({ queryKey });
|
|
273
|
+
}, [queryClient, queryKey]);
|
|
176
274
|
const logout = react.useCallback(async (redirectOnLogout = true) => {
|
|
177
|
-
|
|
178
|
-
(_a = activeRequestRef.current) == null ? void 0 : _a.abort();
|
|
275
|
+
await queryClient.cancelQueries({ queryKey });
|
|
179
276
|
try {
|
|
180
277
|
await fetch(ssoUrls.logout, {
|
|
181
278
|
method: "POST",
|
|
@@ -184,25 +281,36 @@ function AthenaSSOProvider({
|
|
|
184
281
|
} catch (logoutError) {
|
|
185
282
|
console.error("[AthenaSDK] Failed to invalidate SSO session during logout", logoutError);
|
|
186
283
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
284
|
+
queryClient.setQueryData(queryKey, {
|
|
285
|
+
kind: "no_session",
|
|
286
|
+
reason: "unauthenticated",
|
|
287
|
+
signedOut: true
|
|
288
|
+
});
|
|
190
289
|
if (redirectOnLogout && typeof window !== "undefined") {
|
|
191
290
|
window.location.assign(ssoUrls.login);
|
|
192
291
|
}
|
|
193
|
-
}, [ssoUrls.login, ssoUrls.logout]);
|
|
194
|
-
const
|
|
195
|
-
|
|
292
|
+
}, [queryClient, queryKey, ssoUrls.login, ssoUrls.logout]);
|
|
293
|
+
const sessionRef = react.useRef(session);
|
|
294
|
+
sessionRef.current = session;
|
|
196
295
|
const getToken = react.useCallback(
|
|
197
296
|
async ({ force = false } = {}) => {
|
|
198
297
|
var _a;
|
|
199
298
|
if (force) {
|
|
200
|
-
|
|
201
|
-
|
|
299
|
+
try {
|
|
300
|
+
const renewed = await queryClient.fetchQuery({
|
|
301
|
+
queryKey,
|
|
302
|
+
queryFn,
|
|
303
|
+
staleTime: 0,
|
|
304
|
+
retry: false,
|
|
305
|
+
networkMode: "always"
|
|
306
|
+
});
|
|
307
|
+
if (renewed.kind === "session") return renewed.data.accessToken;
|
|
308
|
+
} catch {
|
|
309
|
+
}
|
|
202
310
|
}
|
|
203
|
-
return ((_a =
|
|
311
|
+
return ((_a = sessionRef.current) == null ? void 0 : _a.accessToken) ?? null;
|
|
204
312
|
},
|
|
205
|
-
[
|
|
313
|
+
[queryClient, queryKey, queryFn]
|
|
206
314
|
);
|
|
207
315
|
const authState = react.useMemo(() => {
|
|
208
316
|
if (loading) {
|
|
@@ -217,19 +325,7 @@ function AthenaSSOProvider({
|
|
|
217
325
|
getToken
|
|
218
326
|
};
|
|
219
327
|
}
|
|
220
|
-
if (
|
|
221
|
-
const { user: user2, orgs: orgs2, accessToken: accessToken2 } = mapSSOResponse(ssoData);
|
|
222
|
-
return {
|
|
223
|
-
isLoggedIn: true,
|
|
224
|
-
isLoading: false,
|
|
225
|
-
user: user2,
|
|
226
|
-
accessToken: accessToken2,
|
|
227
|
-
orgs: orgs2,
|
|
228
|
-
logout,
|
|
229
|
-
getToken
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
if (error || !ssoData) {
|
|
328
|
+
if (!session) {
|
|
233
329
|
return {
|
|
234
330
|
isLoggedIn: false,
|
|
235
331
|
isLoading: false,
|
|
@@ -240,7 +336,7 @@ function AthenaSSOProvider({
|
|
|
240
336
|
getToken
|
|
241
337
|
};
|
|
242
338
|
}
|
|
243
|
-
const { user, orgs, accessToken } = mapSSOResponse(
|
|
339
|
+
const { user, orgs, accessToken } = mapSSOResponse(session);
|
|
244
340
|
return {
|
|
245
341
|
isLoggedIn: true,
|
|
246
342
|
isLoading: false,
|
|
@@ -250,11 +346,14 @@ function AthenaSSOProvider({
|
|
|
250
346
|
logout,
|
|
251
347
|
getToken
|
|
252
348
|
};
|
|
253
|
-
}, [loading,
|
|
349
|
+
}, [loading, session, logout, getToken]);
|
|
254
350
|
if (authState.isLoading) {
|
|
255
351
|
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: displayWhileLoading ?? null });
|
|
256
352
|
}
|
|
257
353
|
if (!authState.isLoggedIn) {
|
|
354
|
+
if (typeof displayOnError === "function") {
|
|
355
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: displayOnError({ reason: reason ?? "unauthenticated", retry }) });
|
|
356
|
+
}
|
|
258
357
|
return displayOnError ? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: displayOnError }) : null;
|
|
259
358
|
}
|
|
260
359
|
return /* @__PURE__ */ jsxRuntime.jsx(AthenaAuthContext.AthenaAuthContext.Provider, { value: authState, children });
|