opencode-a2a 0.3.1__py3-none-any.whl

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.
@@ -0,0 +1,948 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from ..profile.runtime import SESSION_SHELL_TOGGLE, RuntimeProfile
7
+
8
+ SHARED_SESSION_BINDING_FIELD = "metadata.shared.session.id"
9
+ SHARED_SESSION_METADATA_FIELD = "metadata.shared.session"
10
+ SHARED_MODEL_SELECTION_FIELD = "metadata.shared.model"
11
+ SHARED_STREAM_METADATA_FIELD = "metadata.shared.stream"
12
+ SHARED_PROGRESS_METADATA_FIELD = "metadata.shared.progress"
13
+ SHARED_INTERRUPT_METADATA_FIELD = "metadata.shared.interrupt"
14
+ SHARED_USAGE_METADATA_FIELD = "metadata.shared.usage"
15
+ OPENCODE_DIRECTORY_METADATA_FIELD = "metadata.opencode.directory"
16
+
17
+ SESSION_BINDING_EXTENSION_URI = "urn:a2a:session-binding/v1"
18
+ MODEL_SELECTION_EXTENSION_URI = "urn:a2a:model-selection/v1"
19
+ STREAMING_EXTENSION_URI = "urn:a2a:stream-hints/v1"
20
+ SESSION_QUERY_EXTENSION_URI = "urn:opencode-a2a:session-query/v1"
21
+ PROVIDER_DISCOVERY_EXTENSION_URI = "urn:opencode-a2a:provider-discovery/v1"
22
+ INTERRUPT_CALLBACK_EXTENSION_URI = "urn:a2a:interactive-interrupt/v1"
23
+ COMPATIBILITY_PROFILE_EXTENSION_URI = "urn:a2a:compatibility-profile/v1"
24
+ WIRE_CONTRACT_EXTENSION_URI = "urn:a2a:wire-contract/v1"
25
+ SERVICE_BEHAVIOR_CLASSIFICATION = "service-level-semantic-enhancement"
26
+ CANCEL_IDEMPOTENCY_BEHAVIOR = "return_current_terminal_task"
27
+ TERMINAL_RESUBSCRIBE_BEHAVIOR = "replay_terminal_task_once_then_close"
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class SessionQueryMethodContract:
32
+ method: str
33
+ required_params: tuple[str, ...] = ()
34
+ optional_params: tuple[str, ...] = ()
35
+ unsupported_params: tuple[str, ...] = ()
36
+ result_fields: tuple[str, ...] = ()
37
+ items_type: str | None = None
38
+ notification_response_status: int | None = None
39
+ pagination_mode: str | None = None
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class InterruptMethodContract:
44
+ method: str
45
+ required_params: tuple[str, ...] = ()
46
+ optional_params: tuple[str, ...] = ()
47
+ notification_response_status: int | None = None
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class ProviderDiscoveryMethodContract:
52
+ method: str
53
+ required_params: tuple[str, ...] = ()
54
+ optional_params: tuple[str, ...] = ()
55
+ result_fields: tuple[str, ...] = ()
56
+ items_type: str | None = None
57
+ notification_response_status: int | None = None
58
+
59
+
60
+ PROMPT_ASYNC_REQUEST_REQUIRED_FIELDS: tuple[str, ...] = ("parts",)
61
+ PROMPT_ASYNC_REQUEST_OPTIONAL_FIELDS: tuple[str, ...] = (
62
+ "messageID",
63
+ "model",
64
+ "agent",
65
+ "noReply",
66
+ "tools",
67
+ "format",
68
+ "system",
69
+ "variant",
70
+ )
71
+ PROMPT_ASYNC_REQUEST_ALLOWED_FIELDS: tuple[str, ...] = (
72
+ *PROMPT_ASYNC_REQUEST_REQUIRED_FIELDS,
73
+ *PROMPT_ASYNC_REQUEST_OPTIONAL_FIELDS,
74
+ )
75
+ COMMAND_REQUEST_REQUIRED_FIELDS: tuple[str, ...] = ("command", "arguments")
76
+ COMMAND_REQUEST_OPTIONAL_FIELDS: tuple[str, ...] = (
77
+ "messageID",
78
+ "agent",
79
+ "model",
80
+ "variant",
81
+ "parts",
82
+ )
83
+ COMMAND_REQUEST_ALLOWED_FIELDS: tuple[str, ...] = (
84
+ *COMMAND_REQUEST_REQUIRED_FIELDS,
85
+ *COMMAND_REQUEST_OPTIONAL_FIELDS,
86
+ )
87
+ SHELL_REQUEST_REQUIRED_FIELDS: tuple[str, ...] = ("agent", "command")
88
+ SHELL_REQUEST_OPTIONAL_FIELDS: tuple[str, ...] = ("model",)
89
+ SHELL_REQUEST_ALLOWED_FIELDS: tuple[str, ...] = (
90
+ *SHELL_REQUEST_REQUIRED_FIELDS,
91
+ *SHELL_REQUEST_OPTIONAL_FIELDS,
92
+ )
93
+
94
+ SESSION_QUERY_PAGINATION_MODE = "limit"
95
+ SESSION_QUERY_PAGINATION_BEHAVIOR = "passthrough"
96
+ SESSION_QUERY_DEFAULT_LIMIT = 20
97
+ SESSION_QUERY_MAX_LIMIT = 100
98
+ SESSION_QUERY_PAGINATION_PARAMS: tuple[str, ...] = ("limit",)
99
+ SESSION_QUERY_PAGINATION_UNSUPPORTED: tuple[str, ...] = ("cursor", "page", "size")
100
+
101
+ SESSION_QUERY_METHOD_CONTRACTS: dict[str, SessionQueryMethodContract] = {
102
+ "list_sessions": SessionQueryMethodContract(
103
+ method="opencode.sessions.list",
104
+ optional_params=("limit", "query.limit"),
105
+ unsupported_params=SESSION_QUERY_PAGINATION_UNSUPPORTED,
106
+ result_fields=("items",),
107
+ items_type="Task[]",
108
+ notification_response_status=204,
109
+ pagination_mode=SESSION_QUERY_PAGINATION_MODE,
110
+ ),
111
+ "get_session_messages": SessionQueryMethodContract(
112
+ method="opencode.sessions.messages.list",
113
+ required_params=("session_id",),
114
+ optional_params=("limit", "query.limit"),
115
+ unsupported_params=SESSION_QUERY_PAGINATION_UNSUPPORTED,
116
+ result_fields=("items",),
117
+ items_type="Message[]",
118
+ notification_response_status=204,
119
+ pagination_mode=SESSION_QUERY_PAGINATION_MODE,
120
+ ),
121
+ "prompt_async": SessionQueryMethodContract(
122
+ method="opencode.sessions.prompt_async",
123
+ required_params=("session_id", "request.parts"),
124
+ optional_params=(
125
+ "request.messageID",
126
+ "request.model",
127
+ "request.agent",
128
+ "request.noReply",
129
+ "request.tools",
130
+ "request.format",
131
+ "request.system",
132
+ "request.variant",
133
+ OPENCODE_DIRECTORY_METADATA_FIELD,
134
+ ),
135
+ result_fields=("ok", "session_id"),
136
+ notification_response_status=204,
137
+ ),
138
+ "command": SessionQueryMethodContract(
139
+ method="opencode.sessions.command",
140
+ required_params=("session_id", "request.command", "request.arguments"),
141
+ optional_params=(
142
+ "request.messageID",
143
+ "request.agent",
144
+ "request.model",
145
+ "request.variant",
146
+ "request.parts",
147
+ OPENCODE_DIRECTORY_METADATA_FIELD,
148
+ ),
149
+ result_fields=("item",),
150
+ notification_response_status=204,
151
+ ),
152
+ "shell": SessionQueryMethodContract(
153
+ method="opencode.sessions.shell",
154
+ required_params=("session_id", "request.agent", "request.command"),
155
+ optional_params=("request.model", OPENCODE_DIRECTORY_METADATA_FIELD),
156
+ result_fields=("item",),
157
+ notification_response_status=204,
158
+ ),
159
+ }
160
+
161
+ SESSION_QUERY_METHODS: dict[str, str] = {
162
+ key: contract.method for key, contract in SESSION_QUERY_METHOD_CONTRACTS.items()
163
+ }
164
+ SESSION_CONTROL_METHOD_KEYS: tuple[str, ...] = ("prompt_async", "command", "shell")
165
+ SESSION_CONTROL_METHODS: dict[str, str] = {
166
+ key: SESSION_QUERY_METHODS[key] for key in SESSION_CONTROL_METHOD_KEYS
167
+ }
168
+
169
+ CORE_JSONRPC_METHODS: tuple[str, ...] = (
170
+ "message/send",
171
+ "message/stream",
172
+ "tasks/get",
173
+ "tasks/cancel",
174
+ "tasks/resubscribe",
175
+ )
176
+ CORE_HTTP_ENDPOINTS: tuple[str, ...] = (
177
+ "POST /v1/message:send",
178
+ "POST /v1/message:stream",
179
+ "GET /v1/tasks/{id}",
180
+ "POST /v1/tasks/{id}:cancel",
181
+ "GET /v1/tasks/{id}:subscribe",
182
+ )
183
+ WIRE_CONTRACT_UNSUPPORTED_METHOD_DATA_FIELDS: tuple[str, ...] = (
184
+ "type",
185
+ "method",
186
+ "supported_methods",
187
+ "protocol_version",
188
+ )
189
+
190
+ SESSION_QUERY_ERROR_BUSINESS_CODES: dict[str, int] = {
191
+ "SESSION_NOT_FOUND": -32001,
192
+ "SESSION_FORBIDDEN": -32006,
193
+ "UPSTREAM_UNREACHABLE": -32002,
194
+ "UPSTREAM_HTTP_ERROR": -32003,
195
+ "UPSTREAM_PAYLOAD_ERROR": -32005,
196
+ }
197
+ SESSION_QUERY_ERROR_DATA_FIELDS: tuple[str, ...] = (
198
+ "type",
199
+ "method",
200
+ "session_id",
201
+ "upstream_status",
202
+ "detail",
203
+ )
204
+ SESSION_QUERY_INVALID_PARAMS_DATA_FIELDS: tuple[str, ...] = (
205
+ "type",
206
+ "field",
207
+ "fields",
208
+ "supported",
209
+ "unsupported",
210
+ )
211
+
212
+ INTERRUPT_CALLBACK_METHOD_CONTRACTS: dict[str, InterruptMethodContract] = {
213
+ "reply_permission": InterruptMethodContract(
214
+ method="a2a.interrupt.permission.reply",
215
+ required_params=("request_id", "reply"),
216
+ optional_params=("message", "metadata"),
217
+ notification_response_status=204,
218
+ ),
219
+ "reply_question": InterruptMethodContract(
220
+ method="a2a.interrupt.question.reply",
221
+ required_params=("request_id", "answers"),
222
+ optional_params=("metadata",),
223
+ notification_response_status=204,
224
+ ),
225
+ "reject_question": InterruptMethodContract(
226
+ method="a2a.interrupt.question.reject",
227
+ required_params=("request_id",),
228
+ optional_params=("metadata",),
229
+ notification_response_status=204,
230
+ ),
231
+ }
232
+
233
+ INTERRUPT_CALLBACK_METHODS: dict[str, str] = {
234
+ key: contract.method for key, contract in INTERRUPT_CALLBACK_METHOD_CONTRACTS.items()
235
+ }
236
+
237
+ PROVIDER_DISCOVERY_METHOD_CONTRACTS: dict[str, ProviderDiscoveryMethodContract] = {
238
+ "list_providers": ProviderDiscoveryMethodContract(
239
+ method="opencode.providers.list",
240
+ result_fields=("items", "default_by_provider", "connected"),
241
+ items_type="ProviderSummary[]",
242
+ notification_response_status=204,
243
+ ),
244
+ "list_models": ProviderDiscoveryMethodContract(
245
+ method="opencode.models.list",
246
+ optional_params=("provider_id",),
247
+ result_fields=("items", "default_by_provider", "connected"),
248
+ items_type="ModelSummary[]",
249
+ notification_response_status=204,
250
+ ),
251
+ }
252
+
253
+ PROVIDER_DISCOVERY_METHODS: dict[str, str] = {
254
+ key: contract.method for key, contract in PROVIDER_DISCOVERY_METHOD_CONTRACTS.items()
255
+ }
256
+
257
+ INTERRUPT_SUCCESS_RESULT_FIELDS: tuple[str, ...] = ("ok", "request_id")
258
+ INTERRUPT_ERROR_BUSINESS_CODES: dict[str, int] = {
259
+ "INTERRUPT_REQUEST_NOT_FOUND": -32004,
260
+ "UPSTREAM_UNREACHABLE": -32002,
261
+ "UPSTREAM_HTTP_ERROR": -32003,
262
+ }
263
+ INTERRUPT_ERROR_TYPES: tuple[str, ...] = (
264
+ "INTERRUPT_REQUEST_NOT_FOUND",
265
+ "INTERRUPT_REQUEST_EXPIRED",
266
+ "INTERRUPT_TYPE_MISMATCH",
267
+ "UPSTREAM_UNREACHABLE",
268
+ "UPSTREAM_HTTP_ERROR",
269
+ )
270
+ INTERRUPT_ERROR_DATA_FIELDS: tuple[str, ...] = ("type", "request_id", "upstream_status")
271
+ INTERRUPT_INVALID_PARAMS_DATA_FIELDS: tuple[str, ...] = (
272
+ "type",
273
+ "field",
274
+ "fields",
275
+ "request_id",
276
+ "expected",
277
+ "actual",
278
+ )
279
+ PROVIDER_DISCOVERY_ERROR_BUSINESS_CODES: dict[str, int] = {
280
+ "UPSTREAM_UNREACHABLE": -32002,
281
+ "UPSTREAM_HTTP_ERROR": -32003,
282
+ "UPSTREAM_PAYLOAD_ERROR": -32005,
283
+ }
284
+ PROVIDER_DISCOVERY_ERROR_DATA_FIELDS: tuple[str, ...] = (
285
+ "type",
286
+ "method",
287
+ "upstream_status",
288
+ "detail",
289
+ )
290
+ PROVIDER_DISCOVERY_INVALID_PARAMS_DATA_FIELDS: tuple[str, ...] = (
291
+ "type",
292
+ "field",
293
+ "fields",
294
+ )
295
+
296
+
297
+ @dataclass(frozen=True)
298
+ class DeploymentConditionalMethod:
299
+ method: str
300
+ enabled: bool
301
+ extension_uri: str
302
+ toggle: str
303
+ reason_when_disabled: str = "disabled_by_configuration"
304
+
305
+ @property
306
+ def availability(self) -> str:
307
+ return "enabled" if self.enabled else "disabled"
308
+
309
+ def control_method_flag(self) -> dict[str, Any]:
310
+ return {
311
+ "enabled_by_default": False,
312
+ "config_key": self.toggle,
313
+ }
314
+
315
+ def method_retention(self) -> dict[str, Any]:
316
+ return {
317
+ "surface": "extension",
318
+ "availability": self.availability,
319
+ "retention": "deployment-conditional",
320
+ "extension_uri": self.extension_uri,
321
+ "toggle": self.toggle,
322
+ }
323
+
324
+ def disabled_wire_contract_entry(self) -> dict[str, str] | None:
325
+ if self.enabled:
326
+ return None
327
+ return {
328
+ "reason": self.reason_when_disabled,
329
+ "toggle": self.toggle,
330
+ }
331
+
332
+
333
+ @dataclass(frozen=True)
334
+ class JsonRpcCapabilitySnapshot:
335
+ conditional_methods: dict[str, DeploymentConditionalMethod]
336
+
337
+ def is_method_enabled(self, method: str) -> bool:
338
+ conditional_method = self.conditional_methods.get(method)
339
+ if conditional_method is None:
340
+ return True
341
+ return conditional_method.enabled
342
+
343
+ def session_query_methods(self) -> dict[str, str]:
344
+ methods = dict(SESSION_QUERY_METHODS)
345
+ if not self.is_method_enabled(SESSION_QUERY_METHODS["shell"]):
346
+ methods.pop("shell", None)
347
+ return methods
348
+
349
+ def session_control_methods(self) -> dict[str, str]:
350
+ methods = dict(SESSION_CONTROL_METHODS)
351
+ if not self.is_method_enabled(SESSION_CONTROL_METHODS["shell"]):
352
+ methods.pop("shell", None)
353
+ return methods
354
+
355
+ def provider_discovery_methods(self) -> dict[str, str]:
356
+ return dict(PROVIDER_DISCOVERY_METHODS)
357
+
358
+ def interrupt_callback_methods(self) -> dict[str, str]:
359
+ return dict(INTERRUPT_CALLBACK_METHODS)
360
+
361
+ def supported_jsonrpc_methods(self) -> list[str]:
362
+ methods = [
363
+ *CORE_JSONRPC_METHODS,
364
+ SESSION_QUERY_METHODS["list_sessions"],
365
+ SESSION_QUERY_METHODS["get_session_messages"],
366
+ SESSION_CONTROL_METHODS["prompt_async"],
367
+ SESSION_CONTROL_METHODS["command"],
368
+ *PROVIDER_DISCOVERY_METHODS.values(),
369
+ *INTERRUPT_CALLBACK_METHODS.values(),
370
+ ]
371
+ if self.is_method_enabled(SESSION_CONTROL_METHODS["shell"]):
372
+ methods.append(SESSION_CONTROL_METHODS["shell"])
373
+ return methods
374
+
375
+ def extension_jsonrpc_methods(self) -> list[str]:
376
+ methods = [
377
+ SESSION_QUERY_METHODS["list_sessions"],
378
+ SESSION_QUERY_METHODS["get_session_messages"],
379
+ SESSION_CONTROL_METHODS["prompt_async"],
380
+ SESSION_CONTROL_METHODS["command"],
381
+ *PROVIDER_DISCOVERY_METHODS.values(),
382
+ *INTERRUPT_CALLBACK_METHODS.values(),
383
+ ]
384
+ if self.is_method_enabled(SESSION_CONTROL_METHODS["shell"]):
385
+ methods.append(SESSION_CONTROL_METHODS["shell"])
386
+ return methods
387
+
388
+ def conditionally_available_methods(self) -> dict[str, dict[str, str]]:
389
+ return {
390
+ method: disabled_entry
391
+ for method, conditional_method in self.conditional_methods.items()
392
+ if (disabled_entry := conditional_method.disabled_wire_contract_entry()) is not None
393
+ }
394
+
395
+ def control_method_flags(self) -> dict[str, dict[str, Any]]:
396
+ return {
397
+ method: conditional_method.control_method_flag()
398
+ for method, conditional_method in self.conditional_methods.items()
399
+ if method in SESSION_CONTROL_METHODS.values()
400
+ }
401
+
402
+ def conditional_method_retention(self) -> dict[str, dict[str, Any]]:
403
+ return {
404
+ method: conditional_method.method_retention()
405
+ for method, conditional_method in self.conditional_methods.items()
406
+ }
407
+
408
+
409
+ def build_capability_snapshot(*, runtime_profile: RuntimeProfile) -> JsonRpcCapabilitySnapshot:
410
+ return JsonRpcCapabilitySnapshot(
411
+ conditional_methods={
412
+ SESSION_CONTROL_METHODS["shell"]: DeploymentConditionalMethod(
413
+ method=SESSION_CONTROL_METHODS["shell"],
414
+ enabled=runtime_profile.session_shell_enabled,
415
+ extension_uri=SESSION_QUERY_EXTENSION_URI,
416
+ toggle=SESSION_SHELL_TOGGLE,
417
+ )
418
+ }
419
+ )
420
+
421
+
422
+ def _build_method_contract_params(
423
+ *,
424
+ required: tuple[str, ...],
425
+ optional: tuple[str, ...],
426
+ unsupported: tuple[str, ...],
427
+ ) -> dict[str, list[str]]:
428
+ params: dict[str, list[str]] = {}
429
+ if required:
430
+ params["required"] = list(required)
431
+ if optional:
432
+ params["optional"] = list(optional)
433
+ if unsupported:
434
+ params["unsupported"] = list(unsupported)
435
+ return params
436
+
437
+
438
+ def build_session_binding_extension_params(
439
+ *,
440
+ runtime_profile: RuntimeProfile,
441
+ ) -> dict[str, Any]:
442
+ return {
443
+ "metadata_field": SHARED_SESSION_BINDING_FIELD,
444
+ "behavior": "prefer_metadata_binding_else_create_session",
445
+ "supported_metadata": [
446
+ "shared.session.id",
447
+ "opencode.directory",
448
+ ],
449
+ "provider_private_metadata": ["opencode.directory"],
450
+ "profile": runtime_profile.summary_dict(),
451
+ "notes": [
452
+ (
453
+ "If metadata.shared.session.id is provided, the server will send the "
454
+ "message to that upstream session."
455
+ ),
456
+ (
457
+ "Otherwise, the server will create a new upstream session and cache "
458
+ "the (identity, contextId)->session_id mapping in memory with TTL."
459
+ ),
460
+ ],
461
+ }
462
+
463
+
464
+ def build_model_selection_extension_params(
465
+ *,
466
+ runtime_profile: RuntimeProfile,
467
+ ) -> dict[str, Any]:
468
+ return {
469
+ "metadata_field": SHARED_MODEL_SELECTION_FIELD,
470
+ "behavior": "prefer_metadata_model_else_upstream_default",
471
+ "applies_to_methods": ["message/send", "message/stream"],
472
+ "supported_metadata": [
473
+ "shared.model.providerID",
474
+ "shared.model.modelID",
475
+ ],
476
+ "provider_private_metadata": [],
477
+ "profile": runtime_profile.summary_dict(),
478
+ "fields": {
479
+ "providerID": f"{SHARED_MODEL_SELECTION_FIELD}.providerID",
480
+ "modelID": f"{SHARED_MODEL_SELECTION_FIELD}.modelID",
481
+ },
482
+ "notes": [
483
+ (
484
+ "If both metadata.shared.model.providerID and metadata.shared.model.modelID "
485
+ "are non-empty strings, the server will override the upstream model for "
486
+ "this request only."
487
+ ),
488
+ (
489
+ "If shared model metadata is missing, partial, or invalid, the server "
490
+ "falls back to the upstream OpenCode default behavior."
491
+ ),
492
+ ],
493
+ }
494
+
495
+
496
+ def build_streaming_extension_params() -> dict[str, Any]:
497
+ return {
498
+ "artifact_metadata_field": SHARED_STREAM_METADATA_FIELD,
499
+ "status_metadata_field": SHARED_STREAM_METADATA_FIELD,
500
+ "progress_metadata_field": SHARED_PROGRESS_METADATA_FIELD,
501
+ "interrupt_metadata_field": SHARED_INTERRUPT_METADATA_FIELD,
502
+ "session_metadata_field": SHARED_SESSION_METADATA_FIELD,
503
+ "usage_metadata_field": SHARED_USAGE_METADATA_FIELD,
504
+ "block_types": ["text", "reasoning", "tool_call"],
505
+ "block_contracts": {
506
+ "text": {
507
+ "part_kind": "text",
508
+ "payload_field": "artifact.parts[].text",
509
+ },
510
+ "reasoning": {
511
+ "part_kind": "text",
512
+ "payload_field": "artifact.parts[].text",
513
+ },
514
+ "tool_call": {
515
+ "part_kind": "data",
516
+ "payload_field": "artifact.parts[].data",
517
+ "payload_fields": {
518
+ "call_id": "artifact.parts[].data.call_id",
519
+ "tool": "artifact.parts[].data.tool",
520
+ "status": "artifact.parts[].data.status",
521
+ "title": "artifact.parts[].data.title",
522
+ "subtitle": "artifact.parts[].data.subtitle",
523
+ "input": "artifact.parts[].data.input",
524
+ "output": "artifact.parts[].data.output",
525
+ "error": "artifact.parts[].data.error",
526
+ },
527
+ },
528
+ },
529
+ "stream_fields": {
530
+ "block_type": f"{SHARED_STREAM_METADATA_FIELD}.block_type",
531
+ "source": f"{SHARED_STREAM_METADATA_FIELD}.source",
532
+ "message_id": f"{SHARED_STREAM_METADATA_FIELD}.message_id",
533
+ "event_id": f"{SHARED_STREAM_METADATA_FIELD}.event_id",
534
+ "sequence": f"{SHARED_STREAM_METADATA_FIELD}.sequence",
535
+ "role": f"{SHARED_STREAM_METADATA_FIELD}.role",
536
+ },
537
+ "progress_fields": {
538
+ "type": f"{SHARED_PROGRESS_METADATA_FIELD}.type",
539
+ "part_id": f"{SHARED_PROGRESS_METADATA_FIELD}.part_id",
540
+ "reason": f"{SHARED_PROGRESS_METADATA_FIELD}.reason",
541
+ "status": f"{SHARED_PROGRESS_METADATA_FIELD}.status",
542
+ "title": f"{SHARED_PROGRESS_METADATA_FIELD}.title",
543
+ "subtitle": f"{SHARED_PROGRESS_METADATA_FIELD}.subtitle",
544
+ },
545
+ "interrupt_fields": {
546
+ "request_id": f"{SHARED_INTERRUPT_METADATA_FIELD}.request_id",
547
+ "type": f"{SHARED_INTERRUPT_METADATA_FIELD}.type",
548
+ "phase": f"{SHARED_INTERRUPT_METADATA_FIELD}.phase",
549
+ "details": f"{SHARED_INTERRUPT_METADATA_FIELD}.details",
550
+ "resolution": f"{SHARED_INTERRUPT_METADATA_FIELD}.resolution",
551
+ },
552
+ "session_fields": {
553
+ "id": f"{SHARED_SESSION_METADATA_FIELD}.id",
554
+ "title": f"{SHARED_SESSION_METADATA_FIELD}.title",
555
+ },
556
+ "usage_fields": {
557
+ "input_tokens": f"{SHARED_USAGE_METADATA_FIELD}.input_tokens",
558
+ "output_tokens": f"{SHARED_USAGE_METADATA_FIELD}.output_tokens",
559
+ "total_tokens": f"{SHARED_USAGE_METADATA_FIELD}.total_tokens",
560
+ "reasoning_tokens": f"{SHARED_USAGE_METADATA_FIELD}.reasoning_tokens",
561
+ "cost": f"{SHARED_USAGE_METADATA_FIELD}.cost",
562
+ "cache_tokens": {
563
+ "read_tokens": f"{SHARED_USAGE_METADATA_FIELD}.cache_tokens.read_tokens",
564
+ "write_tokens": f"{SHARED_USAGE_METADATA_FIELD}.cache_tokens.write_tokens",
565
+ },
566
+ },
567
+ }
568
+
569
+
570
+ def build_session_query_extension_params(
571
+ *,
572
+ runtime_profile: RuntimeProfile,
573
+ context_id_prefix: str,
574
+ ) -> dict[str, Any]:
575
+ capability_snapshot = build_capability_snapshot(runtime_profile=runtime_profile)
576
+ methods = capability_snapshot.session_query_methods()
577
+ control_methods = capability_snapshot.session_control_methods()
578
+ active_session_query_methods = set(methods.values())
579
+
580
+ method_contracts: dict[str, Any] = {}
581
+ pagination_applies_to: list[str] = []
582
+
583
+ for method_contract in SESSION_QUERY_METHOD_CONTRACTS.values():
584
+ if method_contract.method not in active_session_query_methods:
585
+ continue
586
+ params_contract = _build_method_contract_params(
587
+ required=method_contract.required_params,
588
+ optional=method_contract.optional_params,
589
+ unsupported=method_contract.unsupported_params,
590
+ )
591
+ result_contract: dict[str, Any] = {"fields": list(method_contract.result_fields)}
592
+ if method_contract.items_type:
593
+ result_contract["items_type"] = method_contract.items_type
594
+
595
+ contract_doc: dict[str, Any] = {
596
+ "params": params_contract,
597
+ "result": result_contract,
598
+ }
599
+ if method_contract.notification_response_status is not None:
600
+ contract_doc["notification_response_status"] = (
601
+ method_contract.notification_response_status
602
+ )
603
+ method_contracts[method_contract.method] = contract_doc
604
+
605
+ if method_contract.pagination_mode == SESSION_QUERY_PAGINATION_MODE:
606
+ pagination_applies_to.append(method_contract.method)
607
+
608
+ return {
609
+ "methods": methods,
610
+ "control_methods": control_methods,
611
+ "control_method_flags": capability_snapshot.control_method_flags(),
612
+ "profile": runtime_profile.summary_dict(),
613
+ "pagination": {
614
+ "mode": SESSION_QUERY_PAGINATION_MODE,
615
+ "default_limit": SESSION_QUERY_DEFAULT_LIMIT,
616
+ "max_limit": SESSION_QUERY_MAX_LIMIT,
617
+ "behavior": SESSION_QUERY_PAGINATION_BEHAVIOR,
618
+ "params": list(SESSION_QUERY_PAGINATION_PARAMS),
619
+ "applies_to": pagination_applies_to,
620
+ },
621
+ "method_contracts": method_contracts,
622
+ "errors": {
623
+ "business_codes": dict(SESSION_QUERY_ERROR_BUSINESS_CODES),
624
+ "error_data_fields": list(SESSION_QUERY_ERROR_DATA_FIELDS),
625
+ "invalid_params_data_fields": list(SESSION_QUERY_INVALID_PARAMS_DATA_FIELDS),
626
+ },
627
+ "context_semantics": {
628
+ "a2a_context_id_field": "contextId",
629
+ "a2a_context_id_prefix": context_id_prefix,
630
+ "upstream_session_id_field": SHARED_SESSION_BINDING_FIELD,
631
+ },
632
+ }
633
+
634
+
635
+ def build_interrupt_callback_extension_params(
636
+ *,
637
+ runtime_profile: RuntimeProfile,
638
+ ) -> dict[str, Any]:
639
+ method_contracts: dict[str, Any] = {}
640
+ for contract in INTERRUPT_CALLBACK_METHOD_CONTRACTS.values():
641
+ method_contract_doc: dict[str, Any] = {
642
+ "params": _build_method_contract_params(
643
+ required=contract.required_params,
644
+ optional=contract.optional_params,
645
+ unsupported=(),
646
+ ),
647
+ "result": {"fields": list(INTERRUPT_SUCCESS_RESULT_FIELDS)},
648
+ }
649
+ if contract.notification_response_status is not None:
650
+ method_contract_doc["notification_response_status"] = (
651
+ contract.notification_response_status
652
+ )
653
+ method_contracts[contract.method] = method_contract_doc
654
+
655
+ return {
656
+ "methods": dict(INTERRUPT_CALLBACK_METHODS),
657
+ "method_contracts": method_contracts,
658
+ "supported_interrupt_events": [
659
+ "permission.asked",
660
+ "question.asked",
661
+ ],
662
+ "permission_reply_values": ["once", "always", "reject"],
663
+ "question_reply_contract": {
664
+ "answers": "array of answer arrays (same order as asked questions)"
665
+ },
666
+ "request_id_field": f"{SHARED_INTERRUPT_METADATA_FIELD}.request_id",
667
+ "supported_metadata": ["opencode.directory"],
668
+ "provider_private_metadata": ["opencode.directory"],
669
+ "context_fields": {
670
+ "directory": OPENCODE_DIRECTORY_METADATA_FIELD,
671
+ },
672
+ "success_result_fields": list(INTERRUPT_SUCCESS_RESULT_FIELDS),
673
+ "errors": {
674
+ "business_codes": dict(INTERRUPT_ERROR_BUSINESS_CODES),
675
+ "error_types": list(INTERRUPT_ERROR_TYPES),
676
+ "error_data_fields": list(INTERRUPT_ERROR_DATA_FIELDS),
677
+ "invalid_params_data_fields": list(INTERRUPT_INVALID_PARAMS_DATA_FIELDS),
678
+ },
679
+ "profile": runtime_profile.summary_dict(),
680
+ }
681
+
682
+
683
+ def build_provider_discovery_extension_params(
684
+ *,
685
+ runtime_profile: RuntimeProfile,
686
+ ) -> dict[str, Any]:
687
+ method_contracts: dict[str, Any] = {}
688
+
689
+ for method_contract in PROVIDER_DISCOVERY_METHOD_CONTRACTS.values():
690
+ params_contract = _build_method_contract_params(
691
+ required=method_contract.required_params,
692
+ optional=method_contract.optional_params,
693
+ unsupported=(),
694
+ )
695
+ result_contract: dict[str, Any] = {"fields": list(method_contract.result_fields)}
696
+ if method_contract.items_type:
697
+ result_contract["items_type"] = method_contract.items_type
698
+
699
+ contract_doc: dict[str, Any] = {
700
+ "params": params_contract,
701
+ "result": result_contract,
702
+ }
703
+ if method_contract.notification_response_status is not None:
704
+ contract_doc["notification_response_status"] = (
705
+ method_contract.notification_response_status
706
+ )
707
+ method_contracts[method_contract.method] = contract_doc
708
+
709
+ return {
710
+ "methods": dict(PROVIDER_DISCOVERY_METHODS),
711
+ "method_contracts": method_contracts,
712
+ "supported_metadata": ["opencode.directory"],
713
+ "provider_private_metadata": ["opencode.directory"],
714
+ "context_fields": {
715
+ "directory": OPENCODE_DIRECTORY_METADATA_FIELD,
716
+ },
717
+ "provider_item_fields": {
718
+ "provider_id": "items[].provider_id",
719
+ "name": "items[].name",
720
+ "source": "items[].source",
721
+ "connected": "items[].connected",
722
+ "default_model_id": "items[].default_model_id",
723
+ "model_count": "items[].model_count",
724
+ },
725
+ "model_item_fields": {
726
+ "provider_id": "items[].provider_id",
727
+ "model_id": "items[].model_id",
728
+ "name": "items[].name",
729
+ "status": "items[].status",
730
+ "context_window": "items[].context_window",
731
+ "supports_reasoning": "items[].supports_reasoning",
732
+ "supports_tool_call": "items[].supports_tool_call",
733
+ "supports_attachments": "items[].supports_attachments",
734
+ "default": "items[].default",
735
+ "connected": "items[].connected",
736
+ },
737
+ "errors": {
738
+ "business_codes": dict(PROVIDER_DISCOVERY_ERROR_BUSINESS_CODES),
739
+ "error_data_fields": list(PROVIDER_DISCOVERY_ERROR_DATA_FIELDS),
740
+ "invalid_params_data_fields": list(PROVIDER_DISCOVERY_INVALID_PARAMS_DATA_FIELDS),
741
+ },
742
+ "profile": runtime_profile.summary_dict(),
743
+ "notes": [
744
+ (
745
+ "Provider/model discovery is OpenCode-specific and exposed through "
746
+ "provider-private JSON-RPC methods."
747
+ ),
748
+ (
749
+ "The server normalizes upstream provider catalogs into summary records so "
750
+ "downstream callers do not need to parse raw OpenCode payloads."
751
+ ),
752
+ ],
753
+ }
754
+
755
+
756
+ def build_compatibility_profile_params(
757
+ *,
758
+ protocol_version: str,
759
+ runtime_profile: RuntimeProfile,
760
+ ) -> dict[str, Any]:
761
+ capability_snapshot = build_capability_snapshot(runtime_profile=runtime_profile)
762
+ service_behaviors = build_service_behavior_contract_params()
763
+ method_retention: dict[str, dict[str, Any]] = {
764
+ method: {
765
+ "surface": "core",
766
+ "availability": "always",
767
+ "retention": "required",
768
+ }
769
+ for method in CORE_JSONRPC_METHODS
770
+ }
771
+ method_retention.update(
772
+ {
773
+ method: {
774
+ "surface": "extension",
775
+ "availability": "always",
776
+ "retention": "stable",
777
+ "extension_uri": SESSION_QUERY_EXTENSION_URI,
778
+ }
779
+ for method in (
780
+ SESSION_QUERY_METHODS["list_sessions"],
781
+ SESSION_QUERY_METHODS["get_session_messages"],
782
+ SESSION_CONTROL_METHODS["prompt_async"],
783
+ SESSION_CONTROL_METHODS["command"],
784
+ )
785
+ }
786
+ )
787
+ method_retention.update(capability_snapshot.conditional_method_retention())
788
+ method_retention.update(
789
+ {
790
+ method: {
791
+ "surface": "extension",
792
+ "availability": "always",
793
+ "retention": "stable",
794
+ "extension_uri": PROVIDER_DISCOVERY_EXTENSION_URI,
795
+ }
796
+ for method in PROVIDER_DISCOVERY_METHODS.values()
797
+ }
798
+ )
799
+ method_retention.update(
800
+ {
801
+ method: {
802
+ "surface": "extension",
803
+ "availability": "always",
804
+ "retention": "stable",
805
+ "extension_uri": INTERRUPT_CALLBACK_EXTENSION_URI,
806
+ }
807
+ for method in INTERRUPT_CALLBACK_METHODS.values()
808
+ }
809
+ )
810
+ return {
811
+ **runtime_profile.summary_dict(protocol_version=protocol_version),
812
+ "core": {
813
+ "jsonrpc_methods": list(CORE_JSONRPC_METHODS),
814
+ "http_endpoints": list(CORE_HTTP_ENDPOINTS),
815
+ },
816
+ "extension_retention": {
817
+ SESSION_BINDING_EXTENSION_URI: {
818
+ "surface": "core-runtime-metadata",
819
+ "availability": "always",
820
+ "retention": "required",
821
+ },
822
+ MODEL_SELECTION_EXTENSION_URI: {
823
+ "surface": "core-runtime-metadata",
824
+ "availability": "always",
825
+ "retention": "stable",
826
+ },
827
+ STREAMING_EXTENSION_URI: {
828
+ "surface": "core-runtime-metadata",
829
+ "availability": "always",
830
+ "retention": "required",
831
+ },
832
+ SESSION_QUERY_EXTENSION_URI: {
833
+ "surface": "jsonrpc-extension",
834
+ "availability": "always",
835
+ "retention": "stable",
836
+ },
837
+ PROVIDER_DISCOVERY_EXTENSION_URI: {
838
+ "surface": "jsonrpc-extension",
839
+ "availability": "always",
840
+ "retention": "stable",
841
+ },
842
+ INTERRUPT_CALLBACK_EXTENSION_URI: {
843
+ "surface": "jsonrpc-extension",
844
+ "availability": "always",
845
+ "retention": "stable",
846
+ },
847
+ },
848
+ "method_retention": method_retention,
849
+ "service_behaviors": service_behaviors,
850
+ "consumer_guidance": [
851
+ "Treat core A2A methods as the stable interoperability baseline for generic clients.",
852
+ (
853
+ "Treat this deployment as a single-tenant, shared-workspace coding profile; "
854
+ "do not assume per-consumer workspace or tenant isolation."
855
+ ),
856
+ (
857
+ "Treat shared model selection metadata as a stable request-scoped plugin "
858
+ "surface for the main chat path; provider defaults still belong to OpenCode."
859
+ ),
860
+ (
861
+ "Treat opencode.sessions.*, opencode.providers.*, and opencode.models.* as "
862
+ "provider-private operational surfaces rather than portable A2A baseline "
863
+ "capabilities."
864
+ ),
865
+ (
866
+ "Treat a2a.interrupt.* methods as declared shared extensions and opencode.* "
867
+ "methods as vendor-specific extensions that remain stable within the current "
868
+ "major line."
869
+ ),
870
+ (
871
+ "Treat opencode.sessions.shell as deployment-conditional and discover it from "
872
+ "the declared profile and current wire contract before calling it."
873
+ ),
874
+ (
875
+ "Treat declared service behaviors as stable server-level semantic "
876
+ "enhancements layered on top of the core A2A method baseline."
877
+ ),
878
+ ],
879
+ }
880
+
881
+
882
+ def build_wire_contract_params(
883
+ *,
884
+ protocol_version: str,
885
+ runtime_profile: RuntimeProfile,
886
+ ) -> dict[str, Any]:
887
+ capability_snapshot = build_capability_snapshot(runtime_profile=runtime_profile)
888
+ service_behaviors = build_service_behavior_contract_params()
889
+
890
+ return {
891
+ "protocol_version": protocol_version,
892
+ "profile": runtime_profile.summary_dict(protocol_version=protocol_version),
893
+ "preferred_transport": "HTTP+JSON",
894
+ "additional_transports": ["JSON-RPC"],
895
+ "core": {
896
+ "jsonrpc_methods": list(CORE_JSONRPC_METHODS),
897
+ "http_endpoints": list(CORE_HTTP_ENDPOINTS),
898
+ },
899
+ "extensions": {
900
+ "jsonrpc_methods": capability_snapshot.extension_jsonrpc_methods(),
901
+ "conditionally_available_methods": (
902
+ capability_snapshot.conditionally_available_methods()
903
+ ),
904
+ "extension_uris": [
905
+ SESSION_BINDING_EXTENSION_URI,
906
+ MODEL_SELECTION_EXTENSION_URI,
907
+ STREAMING_EXTENSION_URI,
908
+ SESSION_QUERY_EXTENSION_URI,
909
+ PROVIDER_DISCOVERY_EXTENSION_URI,
910
+ INTERRUPT_CALLBACK_EXTENSION_URI,
911
+ ],
912
+ },
913
+ "all_jsonrpc_methods": capability_snapshot.supported_jsonrpc_methods(),
914
+ "service_behaviors": service_behaviors,
915
+ "unsupported_method_error": {
916
+ "code": -32601,
917
+ "type": "METHOD_NOT_SUPPORTED",
918
+ "data_fields": list(WIRE_CONTRACT_UNSUPPORTED_METHOD_DATA_FIELDS),
919
+ },
920
+ }
921
+
922
+
923
+ def build_service_behavior_contract_params() -> dict[str, Any]:
924
+ return {
925
+ "classification": SERVICE_BEHAVIOR_CLASSIFICATION,
926
+ "methods": {
927
+ "tasks/cancel": {
928
+ "baseline": "core",
929
+ "retention": "stable",
930
+ "idempotency": {
931
+ "already_canceled": {
932
+ "behavior": CANCEL_IDEMPOTENCY_BEHAVIOR,
933
+ "returns_current_state": "canceled",
934
+ "error": None,
935
+ }
936
+ },
937
+ },
938
+ "tasks/resubscribe": {
939
+ "baseline": "core",
940
+ "retention": "stable",
941
+ "terminal_state_behavior": {
942
+ "behavior": TERMINAL_RESUBSCRIBE_BEHAVIOR,
943
+ "delivery": "single_task_snapshot",
944
+ "closes_stream": True,
945
+ },
946
+ },
947
+ },
948
+ }