diff --git a/.repository-projection.json b/.repository-projection.json index 9220a6d..54cdc21 100644 --- a/.repository-projection.json +++ b/.repository-projection.json @@ -3,11 +3,11 @@ "projection": "examples", "projectionSchemaVersion": 1, "sourceRepository": "dx-corp/mono", - "sourceSha": "0405bd95fc12ed2ca79294c8a91340aec60f7f17", + "sourceSha": "a971433d12b40d9042b3a64c2c074fa8fc398e69", "destinationRepository": "dx-corp/examples", - "priorProjectedBase": "316ede54390995c61fd36ef90b006634789fc143", + "priorProjectedBase": "c4df6306c20a8670d6b0046a960f0c5aa020f8bb", "definitionDigest": "ec2a926dbb322ffc1c2329c0b1f56b2b8bc0528c918473ebbcea0ef8234ff2b5", - "toolDigest": "898e8657d9153a2a51d7c283bf83bb3350b5d1e6", - "contentDigest": "6a0afd64349e428a9c866600b11f7d0967ef9c5889676882fbd8828677089f32", + "toolDigest": "ff02edcbb40b0028af10ab01a101e918341f157a", + "contentDigest": "dd893c827fbf83693aa832b8932dd452e30876e77e70477fe8ebae40ed92d678", "publicationEligible": true } diff --git a/examples/account-brief.mjs b/examples/account-brief.mjs index 3fc93c6..1a36fc9 100755 --- a/examples/account-brief.mjs +++ b/examples/account-brief.mjs @@ -61,7 +61,7 @@ async function main() { writeAccess: check.writeAccess, nextAction: check.nextAction, requirements: check.capabilities.map(item => ({ service: item.service, missingRequirements: item.missingRequirements, - reasonCodes: item.missingRequirementStates.map(reason => reason.reasonCode) })), + accessible: item.accessible })), modelReady: check.selectedModel?.ready ?? null, errorKind: check.error?.kind, requestId: check.error?.requestId, })); @@ -86,13 +86,13 @@ async function main() { else if (["approve", "deny"].includes(action)) { const result = await task.result(); const event = result.event; - if (result.status !== "waiting" || !event || event.requestType !== OperatingThreadRequestType.APPROVAL + if (result.status !== "waiting" || !event || event.requestKind !== OperatingThreadRequestType.APPROVAL || event.requestId !== values.request) { console.log(JSON.stringify({ status: "needs_attention", reason: "approval_request_not_current" })); return 2; } await client.controls.respond({ channelId: task.checkpoint().channelId, turnId: result.turnId, - response: { requestId: event.requestId, callId: event.requestCallId, requestType: event.requestType, + response: { requestId: event.requestId, callId: event.callId, requestKind: event.requestKind, action: action === "approve" ? OperatingThreadResponseAction.APPROVE : OperatingThreadResponseAction.DENY }, idempotencyKey: values["decision-key"], }); @@ -121,15 +121,15 @@ async function main() { } } const receipts = result.status === "completed" ? result.receipts : []; - const actions = receipts.map(item => ({ id: item.id, ownerService: item.ownerService, - objectId: item.objectId, kind: item.kind, lifecycleState: item.lifecycleState, - evidenceRefs: item.evidenceRefs.map(ref => ({ resourceType: ref.resourceType, id: ref.id })) })); - const attention = receipts.some(item => ![ReceiptLifecycleState.SUCCEEDED, ReceiptLifecycleState.VERIFIED].includes(item.lifecycleState)); + const actions = receipts.map(item => ({ id: item.id, ownerService: undefined, + objectId: undefined, kind: item.kind, lifecycleState: item.state, + evidenceRefs: item.evidence.map(ref => ({ resourceType: ref.kind, id: ref.id })) })); + const attention = receipts.some(item => ![ReceiptLifecycleState.SUCCEEDED, ReceiptLifecycleState.VERIFIED].includes(item.state)); console.log(JSON.stringify({ status: result.status, turnId: result.turnId, reason: result.reason, body: result.status === "completed" && !structured ? result.body : undefined, brief, receiptIds: receipts.map(item => item.id), actions, actionStatus: !actions.length ? "not_reported" : attention ? "requires_attention" : "owner_reported_success", - requestId: result.event?.requestId, requestType: result.event?.requestType, errorCode: result.turn?.errorCode, + requestId: result.event?.requestId, requestKind: result.event?.requestKind, errorCode: result.turn?.terminalError?.code, })); return result.status === "completed" && !attention ? 0 : 2; } diff --git a/python/deixic_examples/account_brief.py b/python/deixic_examples/account_brief.py index 4fc342e..52dc7ca 100644 --- a/python/deixic_examples/account_brief.py +++ b/python/deixic_examples/account_brief.py @@ -8,7 +8,7 @@ import sys from pathlib import Path -from console.v1 import console_pb2 as pb +from deixic import protocol as pb from deixic import Deixic, DeixicError from .task_result import save @@ -85,12 +85,7 @@ def main() -> int: next_action=check.next_action, requirements=[ dict( - service=item.service, missing_requirements=list(item.missing_requirements), - reason_codes=[ - reason.reason_code - for reason in item.missing_requirement_states - ], ) for item in check.capabilities ], @@ -137,7 +132,7 @@ def persist(state): if ( outcome.status != "waiting" or event is None - or event.request_type != pb.OPERATING_THREAD_REQUEST_TYPE_APPROVAL + or event.request_kind != pb.REQUEST_KIND_APPROVAL or event.request_id != args.request ): print( @@ -152,13 +147,13 @@ def persist(state): client.controls.respond( channel_id=task.checkpoint()["channelId"], turn_id=outcome.turn_id, - response=pb.OperatingThreadResponse( + response=pb.RespondToRequestRequest( request_id=event.request_id, - call_id=event.request_call_id, - request_type=event.request_type, - action=pb.OPERATING_THREAD_RESPONSE_ACTION_APPROVE + call_id=event.call_id, + request_kind=event.request_kind, + action=pb.RESPONSE_ACTION_APPROVE if args.action == "approve" - else pb.OPERATING_THREAD_RESPONSE_ACTION_DENY, + else pb.RESPONSE_ACTION_DENY, ), idempotency_key=args.decision_key, ) @@ -179,7 +174,7 @@ def progress(event): print( json.dumps( dict( - event_id=event.event_id, + id=event.id, turn_id=event.turn_id, kind=event.kind, ) @@ -218,22 +213,20 @@ def progress(event): actions = [ dict( id=item.id, - owner_service=item.owner_service, - object_id=item.object_id, kind=item.kind, - lifecycle_state=item.lifecycle_state, + lifecycle_state=item.state, evidence_refs=[ - dict(resource_type=ref.resource_type, id=ref.id) - for ref in item.evidence_refs + dict(resource_type=ref.kind, id=ref.id) + for ref in item.evidence ], ) for item in outcome.receipts ] attention = any( - item.lifecycle_state + item.state not in ( - pb.RECEIPT_LIFECYCLE_STATE_SUCCEEDED, - pb.RECEIPT_LIFECYCLE_STATE_VERIFIED, + pb.RECEIPT_STATE_SUCCEEDED, + pb.RECEIPT_STATE_VERIFIED, ) for item in outcome.receipts ) @@ -255,10 +248,10 @@ def progress(event): request_id=outcome.event.request_id if outcome.event else None, - request_type=outcome.event.request_type + request_kind=outcome.event.request_kind if outcome.event else None, - error_code=outcome.turn.error_code + error_code=outcome.turn.terminal_error.code if outcome.turn else None, ) diff --git a/python/deixic_examples/task_result.py b/python/deixic_examples/task_result.py index f7313c9..51244de 100644 --- a/python/deixic_examples/task_result.py +++ b/python/deixic_examples/task_result.py @@ -10,7 +10,7 @@ from pathlib import Path from typing import Any -from console.v1 import console_pb2 as pb +from deixic import protocol as pb from deixic import Deixic, DeixicError @@ -23,7 +23,9 @@ def save(path: Path, state: dict[str, Any], *, create: bool = False) -> None: output.flush() os.fsync(output.fileno()) if create: - os.link(temporary, path) # Atomic and exclusive: partial initial files are never visible. + os.link( + temporary, path + ) # Atomic and exclusive: partial initial files are never visible. else: os.replace(temporary, path) directory = os.open(path.parent, os.O_RDONLY) @@ -36,21 +38,38 @@ def save(path: Path, state: dict[str, Any], *, create: bool = False) -> None: os.unlink(temporary) -def prepare(path: Path, *, organization_id: str, workspace_id: str, - base_url: str, channel_id: str, body: str) -> dict[str, Any]: - state = dict(organization_id=organization_id, workspace_id=workspace_id, - base_url=base_url.rstrip("/"), channel_id=channel_id, body=body, - idempotency_key=str(uuid.uuid4()), turn_id="", cursor=0, - turn_state=pb.OPERATING_TURN_STATE_UNSPECIFIED) +def prepare( + path: Path, + *, + organization_id: str, + workspace_id: str, + base_url: str, + channel_id: str, + body: str, +) -> dict[str, Any]: + state = dict( + organization_id=organization_id, + workspace_id=workspace_id, + base_url=base_url.rstrip("/"), + channel_id=channel_id, + body=body, + idempotency_key=str(uuid.uuid4()), + turn_id="", + cursor=0, + turn_state=pb.TURN_STATE_UNSPECIFIED, + ) save(path, state, create=True) return state -def load(path: Path, *, organization_id: str, workspace_id: str, - base_url: str) -> dict[str, Any]: +def load( + path: Path, *, organization_id: str, workspace_id: str, base_url: str +) -> dict[str, Any]: state = json.loads(path.read_text()) if (state["organization_id"], state["workspace_id"], state["base_url"]) != ( - organization_id, workspace_id, base_url.rstrip("/"), + organization_id, + workspace_id, + base_url.rstrip("/"), ): raise ValueError("Checkpoint belongs to a different tenant or Platform URL") return state @@ -59,12 +78,18 @@ def load(path: Path, *, organization_id: str, workspace_id: str, def submit(client: Deixic, path: Path, state: dict[str, Any]) -> None: if state["turn_id"]: raise ValueError("This checkpoint already has an accepted turn; use resume") - accepted = client.messages.send(channel_id=state["channel_id"], body=state["body"], - idempotency_key=state["idempotency_key"]) + accepted = client.messages.send( + channel_id=state["channel_id"], + body=state["body"], + idempotency_key=state["idempotency_key"], + ) if not accepted.accepted_turn.turn_id: raise ValueError("Submission omitted the accepted turn identity") - state.update(turn_id=accepted.accepted_turn.turn_id, cursor=accepted.replay_cursor, - turn_state=accepted.accepted_turn.state) + state.update( + turn_id=accepted.accepted_turn.turn_id, + cursor=accepted.replay_cursor, + turn_state=accepted.accepted_turn.state, + ) save(path, state) @@ -74,9 +99,12 @@ def result(client: Deixic, state: dict[str, Any]) -> dict[str, Any] | None: messages = {} seen_tokens = set() while True: - thread = client.threads.get(channel_id=state["channel_id"], limit=200, - page_token=page_token) - turn = next((item for item in thread.turns if item.turn_id == state["turn_id"]), turn) + thread = client.threads.get( + channel_id=state["channel_id"], limit=200, page_token=page_token + ) + turn = next( + (item for item in thread.turns if item.turn_id == state["turn_id"]), turn + ) messages.update({message.id: message for message in thread.messages}) if not thread.next_page_token: break @@ -87,36 +115,49 @@ def result(client: Deixic, state: dict[str, Any]) -> dict[str, Any] | None: if turn is None: return None state["turn_state"] = turn.state - if turn.state in (pb.OPERATING_TURN_STATE_FAILED, pb.OPERATING_TURN_STATE_INTERRUPTED): - return dict(status="failed" if turn.state == pb.OPERATING_TURN_STATE_FAILED else "interrupted", - turn_id=turn.turn_id, error_code=turn.error_code) - if turn.state != pb.OPERATING_TURN_STATE_COMPLETED: + if turn.state in (pb.TURN_STATE_FAILED, pb.TURN_STATE_INTERRUPTED): + return dict( + status="failed" if turn.state == pb.TURN_STATE_FAILED else "interrupted", + turn_id=turn.turn_id, + error_code=turn.terminal_error.code, + ) + if turn.state != pb.TURN_STATE_COMPLETED: return None message = messages.get(turn.assistant_message_id) - if message is None or message.role != "assistant" or message.channel_id != state["channel_id"]: + if message is None or message.role != pb.MESSAGE_ROLE_ASSISTANT: raise ValueError("Completed turn omitted its linked final assistant message") receipts = [] for receipt_id in message.receipt_ids: - receipt = client.receipts.get(channel_id=state["channel_id"], receipt_id=receipt_id).receipt + receipt = client.receipts.get( + channel_id=state["channel_id"], receipt_id=receipt_id + ).receipt if receipt.id != receipt_id: raise ValueError("Receipt lookup returned a different identity") receipts.append(receipt.id) - return dict(status="completed", turn_id=turn.turn_id, message_id=message.id, - body=message.body, receipt_ids=receipts) + return dict( + status="completed", + turn_id=turn.turn_id, + message_id=message.id, + body=message.body, + receipt_ids=receipts, + ) def apply_page(path: Path, state: dict[str, Any], page: Any) -> None: if page.reset_required: # Replace the old projection; retained events cannot reconstruct it. - turn = next((item for item in page.snapshot_turns if item.turn_id == state["turn_id"]), None) - state["turn_state"] = turn.state if turn else pb.OPERATING_TURN_STATE_UNSPECIFIED + turn = next( + (item for item in page.snapshot_turns if item.turn_id == state["turn_id"]), + None, + ) + state["turn_state"] = turn.state if turn else pb.TURN_STATE_UNSPECIFIED for event in page.events: if event.turn_id != state["turn_id"]: continue terminal = { - pb.OPERATING_THREAD_EVENT_KIND_TURN_COMPLETED: pb.OPERATING_TURN_STATE_COMPLETED, - pb.OPERATING_THREAD_EVENT_KIND_TURN_FAILED: pb.OPERATING_TURN_STATE_FAILED, - pb.OPERATING_THREAD_EVENT_KIND_TURN_INTERRUPTED: pb.OPERATING_TURN_STATE_INTERRUPTED, + pb.EVENT_KIND_TURN_COMPLETED: pb.TURN_STATE_COMPLETED, + pb.EVENT_KIND_TURN_FAILED: pb.TURN_STATE_FAILED, + pb.EVENT_KIND_TURN_INTERRUPTED: pb.TURN_STATE_INTERRUPTED, }.get(event.kind) if terminal is not None: state["turn_state"] = terminal @@ -126,14 +167,18 @@ def apply_page(path: Path, state: dict[str, Any], page: Any) -> None: def resume(client: Deixic, path: Path, state: dict[str, Any]) -> dict[str, Any]: if not state["turn_id"]: - return dict(status="unacknowledged", action="replay the saved request explicitly") + return dict( + status="unacknowledged", action="replay the saved request explicitly" + ) outcome = result(client, state) save(path, state) if outcome is not None: return outcome while True: previous_cursor = state["cursor"] - page = client.events.list(channel_id=state["channel_id"], after_cursor=previous_cursor) + page = client.events.list( + channel_id=state["channel_id"], after_cursor=previous_cursor + ) apply_page(path, state, page) outcome = result(client, state) save(path, state) @@ -143,7 +188,9 @@ def resume(client: Deixic, path: Path, state: dict[str, Any]) -> dict[str, Any]: break if state["cursor"] == previous_cursor: raise ValueError("Event pagination did not advance its cursor") - stream = client.events.watch(channel_id=state["channel_id"], after_cursor=state["cursor"]) + stream = client.events.watch( + channel_id=state["channel_id"], after_cursor=state["cursor"] + ) try: for page in stream: apply_page(path, state, page) @@ -153,8 +200,12 @@ def resume(client: Deixic, path: Path, state: dict[str, Any]) -> dict[str, Any]: return outcome finally: stream.close() - return dict(status="unfinished", reason="watch_eof", turn_id=state["turn_id"], - cursor=state["cursor"]) + return dict( + status="unfinished", + reason="watch_eof", + turn_id=state["turn_id"], + cursor=state["cursor"], + ) def main() -> int: @@ -167,18 +218,32 @@ def main() -> int: organization = os.environ["DEIXIC_ORGANIZATION_ID"] workspace = os.environ["DEIXIC_WORKSPACE_ID"] base_url = os.environ.get("DEIXIC_BASE_URL", "https://app.deixic.com") - client = Deixic(api_key=os.environ["DEIXIC_API_KEY"], organization_id=organization, - workspace_id=workspace, base_url=base_url) + client = Deixic( + api_key=os.environ["DEIXIC_API_KEY"], + organization_id=organization, + workspace_id=workspace, + base_url=base_url, + ) if args.action == "start": if not args.body: parser.error("start requires --body") - state = prepare(args.state, organization_id=organization, workspace_id=workspace, - base_url=base_url, channel_id=args.channel, body=args.body) + state = prepare( + args.state, + organization_id=organization, + workspace_id=workspace, + base_url=base_url, + channel_id=args.channel, + body=args.body, + ) else: if args.body is not None: parser.error("replay and resume use the saved request; omit --body") - state = load(args.state, organization_id=organization, workspace_id=workspace, - base_url=base_url) + state = load( + args.state, + organization_id=organization, + workspace_id=workspace, + base_url=base_url, + ) try: if args.action in ("start", "replay"): submit(client, args.state, state) diff --git a/scripts/distribution-validation.mjs b/scripts/distribution-validation.mjs index 13c5b1b..2fbd4e1 100644 --- a/scripts/distribution-validation.mjs +++ b/scripts/distribution-validation.mjs @@ -20,16 +20,7 @@ const HEX = /^[0-9a-f]{64}$/; // `dx-corp/capobara` fails `invalid provenance toolDigest` against a // perfectly correct tree. const TOOL_DIGEST = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/; -const PROTO = [ - "agentruntime/v1/runtime.proto", "agents/v1/agents.proto", "codex/v1/codex.proto", - "common/v1/analytics.proto", "common/v1/authz.proto", "common/v1/classification.proto", - "common/v1/delivery.proto", "common/v1/entity.proto", "common/v1/risk.proto", - "common/v1/surface.proto", "connectors/v1/connectors.proto", "console/v1/console.proto", - "deixic/v1/deixic.proto", "memory/v1/memory.proto", "meter/v1/meter.proto", - "objectives/v1/objectives.proto", "orbcontrol/v1/orb_control.proto", - "platform/v1/platform.proto", "remoterunner/v1/remoterunner.proto", - "toolexecution/v1/toolexecution.proto", "traces/v1/traces.proto", "vfs/v1/filesystem.proto", -].sort(); +const PROTO = ["deixicpublic/v1/sdk.proto"]; const SKILLS = [ "doc-coauthoring", "frontend-design", "incident-triage", "install-code-review", "mcp-builder", "openai-agent-browser-verify", "pr-review", "release-verification", "security-review", "skill-creator", @@ -154,12 +145,9 @@ async function validateApi(root, files) { } } const surface = JSON.parse(await readFile(join(root, "contracts", "public-surface.json"), "utf8")); - requireValue(surface.service === "deixic.v1.DeixicService" && surface.operations?.length === 8, "Deixic public facade contract changed"); + requireValue(surface.service === "deixicpublic.v1.DeixicPublicService" && surface.operations?.length === 8, "Deixic public facade contract changed"); const rpcs = surface.operations.map(item => item.rpc).sort(); requireValue(new Set(rpcs).size === 8 && surface.operations.filter(item => item.kind === "mutation").every(item => item.requiresIdempotencyKey), "Deixic public operations are invalid"); - const lock = await readFile(join(root, "buf.lock"), "utf8"); - requireValue(lock.includes("buf.build/bufbuild/protovalidate") && lock.includes("buf.build/googleapis/googleapis") - && (lock.match(/digest:\s*b5:[0-9a-f]+/g) ?? []).length === 2, "Buf dependencies are not pinned"); const generation = await readFile(join(root, "buf.gen.yaml"), "utf8"); requireValue(generation.includes("sudorandom-connect-openapi:v0.19.1") && !generation.includes("./scripts/"), "public codegen config is not pinned or is private"); run("buf", ["build"], root);