Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .agents/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
service.
- Do not consume alternate event/history services or Host-private transport
contracts. `ctx.agents` owns create/resume/get and returns live Agent handles.
- Agent Trace consumes only `@cordisx/protocol/sessions/v1` and is honestly
unavailable when the Host service, exact route permission, or subscription
is unavailable or replaced.
- Agent Trace consumes `@cordisx/protocol/sessions/v1` plus only the
`EntityDefinitionBoundSessionEvent` extension from
`@cordisx/protocol/entities/v1`. It never queries `ctx.entities` or relabels
Session history from a mutable current registry. It is honestly unavailable
when the Host service, exact route permission, or subscription is unavailable
or replaced.
- Every Session capability must be optional and dynamically bound by the Host
to the active same-plugin route's exact `:sessionId`; never use an empty or
wildcard scope.
Expand Down
23 changes: 15 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,17 @@ The Agent/Session Runtime ownership model is:
- environment composition and transport bindings remain Host-private and do
not change plugin contracts, permissions, or Session facts.

The contract is `@cordisx/protocol/sessions/v1` at Protocol main commit
`8891722a7735a3bd00bdd5315084b35b748f5e7f`. The Timeline reads a fixed
Session snapshot, pages through its immutable watermark, then follows the
atomic replay/live subscription. Subscription termination clears the view and
reports the terminal code.
The contracts are `@cordisx/protocol/sessions/v1` and the additive
`EntityDefinitionBoundSessionEvent` from `@cordisx/protocol/entities/v1` at
Protocol main commit `c96c290697f9e802a68c6d3bb094fd27d8d00d1e`.
The Timeline reads a fixed Session snapshot, pages through its immutable
watermark, then follows the atomic replay/live subscription. Subscription
termination clears the view and reports the terminal code.

For an entity-backed Session, `entity/definition-bound` is the durable source
of the exact historical definition identity, digest, and definition bytes.
The Timeline projects that persisted binding and never queries `ctx.entities`
or relabels prior Session history from the latest local entity revision.

The plugin consumes no alternate event service, Host-private import, unsafe
cast, raw bridge, adapter, or second ledger.
Expand Down Expand Up @@ -68,6 +74,7 @@ npm run check
`npm run check` runs typecheck, build, focused tests, and
`npm pack --dry-run`. `cordisx.plugin.json` is the immutable package manifest.
The Protocol dependency is pinned to an exact remotely resolvable main commit.
The Host runtime is formally available at CordisX main commit
`d11df2e0d4a2557dc184f4fcec0b7cd8659e289b`. The package remains private until
real-App integration verification is complete.
The entity-backed Host runtime is formally available at CordisX main commit
`ff9cf8b1ba4e4caffa23abbd767dbba0b8884c8a`; Chatroom's entity-backed consumer
is at `dc1d672b93e6b6a3a29961561546957d66955a1a`. The package remains private
until real-App integration verification is complete.
18 changes: 13 additions & 5 deletions README.zh-Hans.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,18 @@ Agent/Session Runtime 的 ownership model 如下:
Session 事实。

正式契约是 Protocol main commit
`8891722a7735a3bd00bdd5315084b35b748f5e7f` 的
`@cordisx/protocol/sessions/v1`。Timeline 先读取固定 Session snapshot,
`c96c290697f9e802a68c6d3bb094fd27d8d00d1e` 的
`@cordisx/protocol/sessions/v1`,以及
`@cordisx/protocol/entities/v1` 中增量定义的
`EntityDefinitionBoundSessionEvent`。Timeline 先读取固定 Session snapshot,
分页读取 immutable watermark,再接续 atomic replay/live subscription;订阅
终止时会清空视图并报告 terminal code。

对 entity-backed Session,`entity/definition-bound` 是历史定义 identity、
digest 与 definition bytes 的唯一持久事实。Timeline 只投影该 Session 事件中
持久化的 binding,绝不查询 `ctx.entities`,也不按最新 local entity revision
重标历史。

插件不消费替代 event service,也不使用 Host-private import、unsafe cast、
raw bridge、额外 adapter 或第二 ledger。

Expand Down Expand Up @@ -54,6 +61,7 @@ npm run check

`npm run check` 包含 typecheck、build、focused tests 与
`npm pack --dry-run`。Protocol dependency 精确 pin 到可从远端解析的 main
commit。Host Runtime 已在 CordisX main commit
`d11df2e0d4a2557dc184f4fcec0b7cd8659e289b` 正式提供;真实 App 集成验证闭合前,
package 保持 private。
commit。entity-backed Host Runtime 已在 CordisX main commit
`ff9cf8b1ba4e4caffa23abbd767dbba0b8884c8a` 正式提供;Chatroom 的 entity-backed
消费已在 main commit `dc1d672b93e6b6a3a29961561546957d66955a1a`
落地。真实 App 集成验证闭合前,package 保持 private。
3 changes: 2 additions & 1 deletion cordisx.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"https://raw.githubusercontent.com/cordisx/cordisx-protocol/main/schemas/session-snapshot.v1.schema.json",
"https://raw.githubusercontent.com/cordisx/cordisx-protocol/main/schemas/session-subscribe-request.v1.schema.json",
"https://raw.githubusercontent.com/cordisx/cordisx-protocol/main/schemas/session-subscription-page.v1.schema.json",
"https://raw.githubusercontent.com/cordisx/cordisx-protocol/main/schemas/session-subscription-close.v1.schema.json"
"https://raw.githubusercontent.com/cordisx/cordisx-protocol/main/schemas/session-subscription-close.v1.schema.json",
"https://raw.githubusercontent.com/cordisx/cordisx-protocol/main/schemas/entity-session-definition-binding.v1.schema.json"
]
},
"dependencies": [],
Expand Down
41 changes: 34 additions & 7 deletions docs/MIGRATION-CHECKPOINT.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ This repository currently owns:
- Host-neutral Timeline model, filtering, grouping, and read-only body view;
- `TraceShowcaseStore`, which is the read-only Timeline state boundary;
- `src/session-store.ts`, which consumes only
`@cordisx/protocol/sessions/v1`, performs immutable snapshot paging, atomic
`@cordisx/protocol/sessions/v1` plus the
`EntityDefinitionBoundSessionEvent` extension from
`@cordisx/protocol/entities/v1`, performs immutable snapshot paging, atomic
replay/live subscription, and fail-closed generation/terminal fencing;
- truthful unavailable state whenever the public Session service or exact
route-bound permission is absent;
Expand All @@ -65,10 +67,12 @@ adapter/ledger, access a raw bridge, or depend on concrete `ctx.agentLoop`.
Formal Protocol dependency:

- repository: `CordisX/cordisx-protocol`;
- main commit: `8891722a7735a3bd00bdd5315084b35b748f5e7f`;
- main commit: `c96c290697f9e802a68c6d3bb094fd27d8d00d1e`;
- pull requests: `https://github.com/cordisx/cordisx-protocol/pull/77` and
`https://github.com/cordisx/cordisx-protocol/pull/78`;
- public entrypoint: `@cordisx/protocol/sessions/v1`;
`https://github.com/cordisx/cordisx-protocol/pull/78`, plus entities/v1
`https://github.com/cordisx/cordisx-protocol/pull/79`;
- public entrypoints: `@cordisx/protocol/sessions/v1` and
`@cordisx/protocol/entities/v1`;
- status: merged to remote main; focused type, conformance, distribution,
diff, naming, and formal-main export readback passed. The broad Protocol
workflow retains its pre-existing Manager v2 AJV strict-union failure. Mono
Expand All @@ -90,11 +94,11 @@ must never manufacture a durable cursor or ledger.
Formal Host dependency:

- repository: `CordisX/cordisx`;
- main commit: `d11df2e0d4a2557dc184f4fcec0b7cd8659e289b`;
- main commit: `ff9cf8b1ba4e4caffa23abbd767dbba0b8884c8a`;
- pull requests: `https://github.com/cordisx/cordisx/pull/224` through
`https://github.com/cordisx/cordisx/pull/231`;
`https://github.com/cordisx/cordisx/pull/236`;
- status: merged to remote main and pinned to Protocol
`8891722a7735a3bd00bdd5315084b35b748f5e7f`; focused Agent/Session runtime,
`c96c290697f9e802a68c6d3bb094fd27d8d00d1e`; focused Agent/Session runtime,
Shell v4 terminal, permission/scope, no-check type, package allowlist, and
diff checks passed. The #231 composition focus passed 14/14 with CLI
`tsc --noCheck`. Its broad workflow stopped at
Expand Down Expand Up @@ -124,6 +128,29 @@ excluded from filesystem watch/import graphs so Vite cannot reinterpret
`cordisx/react`, `cordisx/ui`, or JSX-runtime virtual sources as absolute file
imports; the real transitive source watch graph remains intact.

## ENTITY_DEFINITION_BINDING_READY handoff

Formal entity-backed consumers:

- Host main: `ff9cf8b1ba4e4caffa23abbd767dbba0b8884c8a`
(`https://github.com/cordisx/cordisx/pull/236`);
- Chatroom main: `dc1d672b93e6b6a3a29961561546957d66955a1a`
(`https://github.com/cordisx/plugin-chatroom/pull/10`);
- Protocol main: `c96c290697f9e802a68c6d3bb094fd27d8d00d1e`.

Host persists `entity/definition-bound` as an ignorable SessionEvent when it
creates an entity-backed Session and resumes only from that persisted binding.
Chatroom creates against the exact current Entity identity and resumes an
existing Session with `definitionSource: 'session-persisted'`.

Agent Trace recognizes that one durable extension during the existing
snapshot/replay/live path. Its projection exposes
`binding.resolution.identity`, `binding.resolution.digest`, and
`binding.resolution.definition`. It does not inject or query `ctx.entities`,
consult Chatroom configuration, or relabel historical runs from a current
registry snapshot. Other unknown required events still fail closed; unrelated
extensions remain skippable only when explicitly marked `ignorable: true`.

## Deferred owner actions

Until real-App integration verification succeeds:
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"cordisx": "0.1.0-beta.1"
},
"devDependencies": {
"@cordisx/protocol": "github:cordisx/cordisx-protocol#8891722a7735a3bd00bdd5315084b35b748f5e7f",
"@cordisx/protocol": "github:cordisx/cordisx-protocol#c96c290697f9e802a68c6d3bb094fd27d8d00d1e",
"jsdom": "^26.1.0",
"typescript": "^5.9.2",
"vitest": "^3.2.4"
Expand Down
3 changes: 3 additions & 0 deletions src/react-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,14 @@ function options(values: readonly string[], all: string) {

function Detail({ event }: { readonly event: TraceEvent | undefined }) {
if (event === undefined) return <Text tone="muted">Select an event to inspect its public projection.</Text>
const definition = event.definitionResolution
const fields: readonly [string, unknown][] = [
['Event', event.id], ['Sequence', event.seq], ['Recorded', event.recordedAt],
['Type', event.type], ['Semantic', event.semanticType], ['Phase', event.phase],
['Session', event.sessionId], ['Turn', event.turnId], ['Step', event.stepId], ['Item', event.itemId],
['Message', event.messageId], ['Tool call', event.toolCallId], ['Context', event.contextId],
['Entity', definition?.identity.agentId], ['Entity revision', definition?.identity.revision],
['Entity digest', definition?.digest], ['Definition name', definition?.definition.name],
['Source', `${event.source.kind}:${event.source.id}`],
]
return <><Text as="p"><strong>{event.summary}</strong></Text><dl>{fields.filter(([, value]) => value !== undefined).map(([label, value]) => <Fragment key={label}><dt>{label}</dt><dd>{String(value)}</dd></Fragment>)}</dl>{event.payload === undefined ? null : <pre className="cat-payload">{JSON.stringify(event.payload, null, 2)}</pre>}</>
Expand Down
10 changes: 10 additions & 0 deletions src/session-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
SessionSubscriptionPage,
UserMessage,
} from '@cordisx/protocol/sessions/v1'
import type { EntityDefinitionResolution } from '@cordisx/protocol/entities/v1'
import type {
TraceEvent,
TraceLane,
Expand Down Expand Up @@ -72,6 +73,7 @@ function lifecyclePhase(event: SessionEvent): TracePhase | undefined {
function lane(event: SessionEvent): TraceLane {
switch (event.type) {
case 'user/message': return 'input'
case 'entity/definition-bound':
case 'agent/inbox/spliced': return 'injection'
case 'tool/call':
case 'tool/result':
Expand Down Expand Up @@ -109,6 +111,10 @@ function summary(event: SessionEvent): string {
case 'agent/inbox/spliced': return `Agent ${event.data.target} inbox changed.`
case 'approval/asked': return `Approval requested for ${event.data.toolName}.`
case 'approval/decided': return `Approval ${event.data.outcome}.`
case 'entity/definition-bound': {
const { identity, digest, definition } = event.data.resolution
return `Session definition bound to ${definition.name ?? identity.agentId} (${identity.agentId}@${identity.revision}; ${digest}).`
}
case 'session/end-seed': return 'Persisted Session seed ended.'
default: return unknownEvent(event)
}
Expand Down Expand Up @@ -171,6 +177,9 @@ export function projectSessionEvent(event: SessionEvent): TraceEvent | undefined
: event.type === 'tool/result'
? event.data.message.source.callId
: undefined
const definitionResolution: EntityDefinitionResolution | undefined = event.type === 'entity/definition-bound'
? event.data.resolution
: undefined
return Object.freeze({
id: `session:${event.sessionId}:${event.seq}`,
sessionId: event.sessionId,
Expand All @@ -186,6 +195,7 @@ export function projectSessionEvent(event: SessionEvent): TraceEvent | undefined
...(step === undefined ? {} : { stepId: step }),
...(messageId === undefined ? {} : { messageId }),
...(toolCallId === undefined ? {} : { toolCallId }),
...(definitionResolution === undefined ? {} : { definitionResolution }),
payload: event.data,
})
}
Expand Down
3 changes: 3 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { EntityDefinitionResolution } from '@cordisx/protocol/entities/v1'

export type TraceLane = 'input' | 'model' | 'tools' | 'injection'
export type TracePhase =
| 'opened' | 'resumed' | 'started' | 'updated' | 'completed'
Expand Down Expand Up @@ -28,6 +30,7 @@ export interface TraceEvent {
readonly messageId?: string
readonly toolCallId?: string
readonly contextId?: string
readonly definitionResolution?: EntityDefinitionResolution
readonly payload?: unknown
}

Expand Down
2 changes: 2 additions & 0 deletions test/architecture-guard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ describe('architecture guard', () => {
'ctx.agentEvents',
'ctx.agentHistory',
'ctx.agentLoop',
'ctx.entities',
'agentEvents',
'agentHistory',
'electronBridge',
Expand All @@ -23,6 +24,7 @@ describe('architecture guard', () => {
'getElementById',
'packages/cli/src',
'packages/host',
'EntityRegistry',
]) expect(source).not.toContain(forbidden)
})

Expand Down
86 changes: 86 additions & 0 deletions test/session-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import type {
SessionSubscriptionClosed,
SessionSubscriptionPage,
} from '@cordisx/protocol/sessions/v1'
import type { EntityDefinitionBoundSessionEvent } from '@cordisx/protocol/entities/v1'
import { SessionTraceStore, projectSessionEvent } from '../src/session-store.js'

const schema = 'https://raw.githubusercontent.com/cordisx/cordisx-protocol/main/schemas/session-event.v1.schema.json' as const
const persistedDigest = `sha256:${'a'.repeat(64)}` as const

function event(seq: number, type: SessionEvent['type'], data: unknown): SessionEvent {
return {
Expand All @@ -27,6 +29,40 @@ function event(seq: number, type: SessionEvent['type'], data: unknown): SessionE
} as unknown as SessionEvent
}

function entityDefinitionBoundEvent(seq: number): EntityDefinitionBoundSessionEvent {
const identity = { agentId: 'chatroom.generalist', revision: persistedDigest }
return {
$schema: schema,
contract: 'cordisx.session-event/v1',
schemaVersion: 1,
sessionId: 'session-a',
seq,
time: Date.UTC(2026, 0, 1, 0, 0, seq),
type: 'entity/definition-bound',
ignorable: true,
data: {
source: 'entity-registry',
owner: { profileId: 'profile-a', installationId: 'installation-a', pluginId: 'chatroom' },
resolution: {
identity,
digest: persistedDigest,
definition: {
$schema: 'https://raw.githubusercontent.com/cordisx/cordisx-protocol/main/schemas/agent-definition.v1.schema.json',
contract: 'cordisx.agent-definition/v1',
schemaVersion: 1,
identity,
name: 'Persisted Generalist',
inherit: {
promptSections: 'none', rules: 'none', skills: 'none',
tools: 'none', mcpServers: 'none', runtimeDefaults: 'none',
},
promptSections: [{ sectionId: 'role', kind: 'role', text: 'Persisted role bytes.' }],
},
},
},
}
}

function page(afterSeq: number, snapshotSeq: number, events: readonly SessionEvent[]): SessionEventPage {
const nextAfterSeq = events.at(-1)?.seq ?? afterSeq
return {
Expand Down Expand Up @@ -166,6 +202,33 @@ describe('SessionTraceStore', () => {
expect(harness.unsubscribe).toHaveBeenCalledOnce()
})

it('carries the persisted entity binding through the same snapshot, replay, and live cursor path', async () => {
const harness = sessionHarness({
snapshotSeq: 0,
pages: [page(-1, 0, [entityDefinitionBoundEvent(0)])],
replay: [event(1, 'turn/start', { turn: 1 })],
})
const store = new SessionTraceStore(harness.sessions, 'session-a', 50)
await store.settled()

expect(store.getSnapshot().events.map(item => item.type)).toEqual([
'entity/definition-bound',
'turn/start',
])
expect(store.getSnapshot().events[0]?.definitionResolution?.definition.name).toBe('Persisted Generalist')

await harness.observer()!(subscriptionPage('live', 1, [
event(2, 'user/message', {
id: 'message-user', role: 'user', content: [{ type: 'text', text: 'Continue' }], source: { kind: 'user' },
}),
]))
expect(store.getSnapshot()).toMatchObject({
events: [{ type: 'entity/definition-bound' }, { type: 'turn/start' }, { type: 'user/message' }],
range: { totalAvailable: 3 },
})
store.dispose()
})

it('fails closed and clears rows on a sequence or generation fence violation', async () => {
const harness = sessionHarness({
snapshotSeq: 1,
Expand Down Expand Up @@ -245,6 +308,29 @@ describe('SessionTraceStore', () => {
})

describe('SessionEvent projection', () => {
it('attributes entity-backed history only to the Session-persisted definition binding', () => {
const projected = projectSessionEvent(entityDefinitionBoundEvent(0))

expect(projected).toMatchObject({
type: 'entity/definition-bound',
lane: 'injection',
definitionResolution: {
identity: { agentId: 'chatroom.generalist', revision: persistedDigest },
digest: persistedDigest,
definition: {
name: 'Persisted Generalist',
promptSections: [{ text: 'Persisted role bytes.' }],
},
},
payload: {
source: 'entity-registry',
resolution: { identity: { agentId: 'chatroom.generalist', revision: persistedDigest } },
},
})
expect(projected?.summary).toContain(`chatroom.generalist@${persistedDigest}`)
expect(projected?.summary).not.toContain('Latest local revision')
})

it('projects every durable core variant from the formal v1 vocabulary', () => {
const variants: readonly [SessionEvent['type'], unknown][] = [
['turn/start', { turn: 1 }],
Expand Down
Loading
Loading