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
2 changes: 2 additions & 0 deletions src/iac_code/agui/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1391,6 +1391,8 @@ def _a2a_request_options(
"cleanupOnly": props.cleanup_only,
"rosInvocationId": props.ros_invocation_id,
}
if props.llm_headers is not None:
metadata["llm_headers"] = props.llm_headers
cloud = props.alibaba_cloud
if cloud is not None:
metadata.update(
Expand Down
1 change: 1 addition & 0 deletions src/iac_code/agui/inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class IacCodeForwardedProps(StrictModel):
cwd: str
model: str | None = None
llm_api_key: str | None = Field(default=None, alias="llmApiKey", repr=False)
llm_headers: dict[str, str] | None = Field(default=None, alias="llmHeaders", repr=False)
thinking: ThinkingOptions | None = None
user_id: str | None = Field(default=None, alias="userId")
channel: str | None = None
Expand Down
24 changes: 24 additions & 0 deletions tests/agui/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1085,6 +1085,10 @@ async def test_request_runtime_overrides_are_forwarded_only_as_a2a_metadata(tmp_
{
"model": "qwen-test",
"llmApiKey": "fake-provider-key",
"llmHeaders": {
"Authorization": "Bearer fake-caller-token",
"X-Caller-Session": "session-1",
},
"thinking": {"enabled": True, "effort": "low", "budget": 1024},
"alibabaCloud": {
"accessKeyId": "fake-access-key",
Expand All @@ -1109,13 +1113,33 @@ async def test_request_runtime_overrides_are_forwarded_only_as_a2a_metadata(tmp_
"cleanupOnly": False,
"rosInvocationId": "invocation-1",
"preferredLanguage": "en",
"llm_headers": {
"Authorization": "Bearer fake-caller-token",
"X-Caller-Session": "session-1",
},
"alibaba_cloud_access_key_id": "fake-access-key",
"alibaba_cloud_access_key_secret": "fake-access-secret",
"alibaba_cloud_security_token": "fake-sts-token",
"alibaba_cloud_region_id": "cn-hangzhou",
}


@pytest.mark.asyncio
async def test_empty_llm_headers_are_forwarded_to_clear_a2a_context_binding(tmp_path, monkeypatch) -> None:
monkeypatch.setenv("IAC_CODE_AGUI_ALLOWED_CWDS", str(tmp_path))
fake = FakeA2AClient()
adapter = AguiA2AAdapter(a2a_url="http://a2a/", client=fake)
app = create_app(adapter=adapter)
payload = _payload(tmp_path)
payload["forwardedProps"]["iacCode"]["llmHeaders"] = {}

async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client:
response = await client.post("/", json=payload)

assert response.status_code == 200
assert fake.stream_options[0]["iac_code_metadata"]["llm_headers"] == {}


@pytest.mark.asyncio
async def test_heartbeat_remains_sse_comment_and_not_agui_event(tmp_path, monkeypatch) -> None:
monkeypatch.setenv("IAC_CODE_AGUI_ALLOWED_CWDS", str(tmp_path))
Expand Down
5 changes: 4 additions & 1 deletion tests/agui/test_inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,24 @@
from iac_code.agui.inputs import latest_user_message, parse_forwarded_props, resolve_cwd


def test_forwarded_props_require_request_workspace_and_identity(tmp_path) -> None:
@pytest.mark.parametrize("llm_headers_field", ["llmHeaders", "llm_headers"])
def test_forwarded_props_require_request_workspace_and_identity(tmp_path, llm_headers_field) -> None:
props = parse_forwarded_props(
{
"iacCode": {
"schemaVersion": 1,
"rosInvocationId": "invocation-1",
"cwd": str(tmp_path),
"model": "qwen-test",
llm_headers_field: {"X-Caller-Session": "session-1"},
"runMode": "pipeline",
}
}
)

assert props.iac_code.cwd == str(tmp_path)
assert props.iac_code.model == "qwen-test"
assert props.iac_code.llm_headers == {"X-Caller-Session": "session-1"}
assert props.iac_code.run_mode == "pipeline"


Expand Down
10 changes: 9 additions & 1 deletion tests/agui/test_persistence.py
Original file line number Diff line number Diff line change
Expand Up @@ -1021,6 +1021,7 @@ async def test_persisted_state_excludes_request_messages_and_credentials(tmp_pat
payload["forwardedProps"]["iacCode"].update(
{
"llmApiKey": "llm-secret",
"llmHeaders": {"Authorization": "Bearer header-secret"},
"alibabaCloud": {
"accessKeyId": "ak-secret",
"accessKeySecret": "sk-secret",
Expand All @@ -1035,7 +1036,14 @@ async def test_persisted_state_excludes_request_messages_and_credentials(tmp_pat

thread_path = _thread_state_path(state_dir)
raw = thread_path.read_text(encoding="utf-8")
for secret in ("private-user-message", "llm-secret", "ak-secret", "sk-secret", "sts-secret"):
for secret in (
"private-user-message",
"llm-secret",
"header-secret",
"ak-secret",
"sk-secret",
"sts-secret",
):
assert secret not in raw
if os.name != "nt":
assert stat.S_IMODE(state_dir.stat().st_mode) == 0o700
Expand Down
3 changes: 3 additions & 0 deletions website/docs/agui/protocol-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ This object uses a strict schema; unknown fields are rejected.
| `cwd` | string | Yes | Absolute workspace path |
| `model` | string | No | Per-request model override |
| `llmApiKey` | string | No | Per-request LLM provider key |
| `llmHeaders` / `llm_headers` | object | No | Additional string-to-string HTTP headers for LLM provider calls |
| `thinking.enabled` | boolean | No | Request thinking output |
| `thinking.effort` | string | No | Provider-specific thinking effort |
| `thinking.budget` | positive integer | No | Provider-specific thinking budget |
Expand All @@ -98,6 +99,8 @@ This object uses a strict schema; unknown fields are rejected.
| `alibabaCloud.securityToken` | string | No | Request-local STS token |
| `alibabaCloud.regionId` | string | No | Request-local default region |

`llmHeaders` follows the A2A context-binding rules for `metadata.iac_code.llm_headers`: once supplied, later requests on the same AG-UI thread inherit the headers when the field is omitted. A new map replaces the complete binding, and `{}` clears it. Header values may contain credentials, so the adapter does not persist them; callers must supply them again after the local A2A process restarts.

The initial run and its interrupt resumes must retain the same `rosInvocationId`. A later normal turn may use a new value. Cancellation must use the current execution's value.

A `threadId` is bound to the first request's `cwd` and `userId`; later requests cannot move the same thread to another workspace or caller.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ Das Schema ist strikt; unbekannte Felder werden abgelehnt.
| `cwd` | Zeichenfolge | Ja | Absoluter Arbeitsbereichspfad |
| `model` | Zeichenfolge | Nein | Modellüberschreibung pro Anfrage |
| `llmApiKey` | Zeichenfolge | Nein | LLM-Anbieterschlüssel pro Anfrage |
| `llmHeaders` / `llm_headers` | Objekt | Nein | Zusätzliche HTTP-Header als Zeichenfolge-zu-Zeichenfolge-Zuordnung für Aufrufe des LLM-Anbieters |
| `thinking.enabled` | boolesch | Nein | Reasoning-Ausgabe anfordern |
| `thinking.effort` | Zeichenfolge | Nein | Anbieterspezifischer Reasoning-Aufwand |
| `thinking.budget` | positive Ganzzahl | Nein | Anbieterspezifisches Reasoning-Budget |
Expand All @@ -98,6 +99,8 @@ Das Schema ist strikt; unbekannte Felder werden abgelehnt.
| `alibabaCloud.securityToken` | Zeichenfolge | Nein | Anfragebezogenes STS-Token |
| `alibabaCloud.regionId` | Zeichenfolge | Nein | Anfragebezogene Standardregion |

`llmHeaders` folgt den A2A-Kontextbindungsregeln für `metadata.iac_code.llm_headers`: Nach der ersten Angabe erben spätere Anfragen desselben AG-UI-Threads die Header, wenn das Feld fehlt. Eine neue Zuordnung ersetzt die gesamte Bindung, und `{}` löscht sie. Da Headerwerte Anmeldedaten enthalten können, speichert der Adapter sie nicht dauerhaft; nach einem Neustart des lokalen A2A-Prozesses muss der Aufrufer sie erneut senden.

Der erste Lauf und seine Wiederaufnahmen müssen dieselbe `rosInvocationId` behalten. Eine spätere normale Runde darf einen neuen Wert verwenden. Beim Abbruch ist der Wert der aktuellen Ausführung erforderlich.

Eine `threadId` wird an `cwd` und `userId` der ersten Anfrage gebunden; spätere Anfragen können denselben Thread nicht in einen anderen Arbeitsbereich oder zu einem anderen Aufrufer verschieben.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ El schema es estricto y rechaza campos desconocidos.
| `rosInvocationId` | string | Sí | Identidad de la ejecución, máximo 256 caracteres |
| `cwd` | string | Sí | Workspace absoluto |
| `model` / `llmApiKey` | string | No | Modelo y clave LLM por solicitud |
| `llmHeaders` / `llm_headers` | objeto | No | Headers HTTP adicionales de string a string para las llamadas al proveedor LLM |
| `thinking.enabled/effort/budget` | boolean/string/entero positivo | No | Opciones de thinking |
| `userId` / `channel` | string | No | Identidad y canal del llamante |
| `preferredLanguage` | string | No | Idioma visible, por ejemplo `es` |
Expand All @@ -80,6 +81,8 @@ El schema es estricto y rechaza campos desconocidos.
| `alibabaCloud.securityToken` | string | No | Token STS temporal |
| `alibabaCloud.regionId` | string | No | Región predeterminada |

`llmHeaders` sigue las reglas de vinculación al contexto A2A de `metadata.iac_code.llm_headers`: una vez enviado, las solicitudes posteriores del mismo thread AG-UI heredan los headers cuando se omite el campo. Un nuevo mapa reemplaza toda la vinculación y `{}` la borra. Como los valores pueden contener credenciales, el adaptador no los persiste; el llamante debe volver a enviarlos tras reiniciar el proceso A2A local.

El run inicial y sus Resume conservan el mismo `rosInvocationId`. Un turno normal posterior puede usar otro. El mismo `threadId` queda vinculado al primer `cwd` y `userId`.

## SSE y eventos estándar
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ Le schéma est strict : les champs inconnus sont refusés.
| `cwd` | chaîne | Oui | Chemin absolu de l’espace de travail |
| `model` | chaîne | Non | Modèle choisi pour cette requête |
| `llmApiKey` | chaîne | Non | Clé du fournisseur LLM pour cette requête |
| `llmHeaders` / `llm_headers` | objet | Non | En-têtes HTTP supplémentaires sous forme de chaînes pour les appels au fournisseur LLM |
| `thinking.enabled` | booléen | Non | Demander la sortie du raisonnement |
| `thinking.effort` | chaîne | Non | Effort de raisonnement propre au fournisseur |
| `thinking.budget` | entier positif | Non | Budget de raisonnement propre au fournisseur |
Expand All @@ -98,6 +99,8 @@ Le schéma est strict : les champs inconnus sont refusés.
| `alibabaCloud.securityToken` | chaîne | Non | Jeton STS local à la requête |
| `alibabaCloud.regionId` | chaîne | Non | Région par défaut locale à la requête |

`llmHeaders` suit les règles de liaison au contexte A2A de `metadata.iac_code.llm_headers` : après sa première transmission, les requêtes suivantes du même thread AG-UI héritent des en-têtes si le champ est omis. Une nouvelle table remplace toute la liaison et `{}` l’efface. Comme les valeurs peuvent contenir des identifiants secrets, l’adaptateur ne les conserve pas ; le demandeur doit les renvoyer après le redémarrage du processus A2A local.

L’exécution initiale et ses reprises doivent conserver le même `rosInvocationId`. Un tour normal ultérieur peut utiliser une nouvelle valeur. L’annulation doit employer celle de l’exécution courante.

Le `threadId` est lié aux `cwd` et `userId` de la première requête ; les requêtes suivantes ne peuvent pas déplacer le thread vers un autre espace de travail ou un autre demandeur.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ Authorization: Bearer <token>
| `cwd` | string | はい | ワークスペース絶対パス |
| `model` | string | いいえ | リクエスト単位のモデル上書き |
| `llmApiKey` | string | いいえ | LLM provider key |
| `llmHeaders` / `llm_headers` | object | いいえ | LLM provider 呼び出しに追加する string-to-string の HTTP header |
| `thinking.enabled/effort/budget` | boolean/string/正整数 | いいえ | thinking 設定 |
| `userId` | string | いいえ | telemetry と呼び出し元の識別 |
| `channel` | string | いいえ | チャネルメタデータ |
Expand All @@ -90,6 +91,8 @@ Authorization: Bearer <token>
| `alibabaCloud.securityToken` | string | いいえ | 一時 STS token |
| `alibabaCloud.regionId` | string | いいえ | 既定 region |

`llmHeaders` は `metadata.iac_code.llm_headers` の A2A context binding 規則に従います。一度指定すると、同じ AG-UI thread の後続リクエストはこのフィールドを省略しても header を継承します。新しい map は binding 全体を置き換え、`{}` は binding を消去します。値には認証情報が含まれる可能性があるため adapter は永続化せず、local A2A process の再起動後は呼び出し元が再送する必要があります。

initial run とその Resume は同じ `rosInvocationId` を使います。次の通常ターンでは新しい値を利用できます。Cancel も現在の値が必要です。

同じ `threadId` は最初の `cwd` と `userId` に固定され、後続リクエストで別のワークスペースや呼び出し元へ変更できません。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ O esquema é estrito; campos desconhecidos são rejeitados.
| `cwd` | string | Sim | Caminho absoluto do workspace |
| `model` | string | Não | Substituição do modelo para a solicitação |
| `llmApiKey` | string | Não | Chave do provedor LLM para a solicitação |
| `llmHeaders` / `llm_headers` | objeto | Não | Headers HTTP adicionais de string para string nas chamadas ao provedor LLM |
| `thinking.enabled` | booleano | Não | Solicitar saída de raciocínio |
| `thinking.effort` | string | Não | Esforço de raciocínio específico do provedor |
| `thinking.budget` | inteiro positivo | Não | Orçamento de raciocínio específico do provedor |
Expand All @@ -98,6 +99,8 @@ O esquema é estrito; campos desconhecidos são rejeitados.
| `alibabaCloud.securityToken` | string | Não | Token STS local à solicitação |
| `alibabaCloud.regionId` | string | Não | Região padrão local à solicitação |

`llmHeaders` segue as regras de vínculo ao contexto A2A de `metadata.iac_code.llm_headers`: depois de informado, as solicitações posteriores do mesmo thread AG-UI herdam os headers quando o campo é omitido. Um novo mapa substitui todo o vínculo, e `{}` o limpa. Como os valores podem conter credenciais, o adaptador não os persiste; o chamador deve enviá-los novamente após a reinicialização do processo A2A local.

A execução inicial e suas retomadas devem manter o mesmo `rosInvocationId`. Um turno normal posterior pode usar um novo valor. O cancelamento deve usar o valor da execução atual.

O `threadId` é vinculado aos `cwd` e `userId` da primeira solicitação; solicitações posteriores não podem mover o mesmo thread para outro workspace ou chamador.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ Authorization: Bearer <token>
| `cwd` | string | 是 | 本次执行的绝对工作区路径 |
| `model` | string | 否 | 单次请求覆盖模型 |
| `llmApiKey` | string | 否 | 单次请求覆盖 LLM provider key |
| `llmHeaders` / `llm_headers` | object | 否 | 传给 LLM provider 调用的额外 string-to-string HTTP headers |
| `thinking.enabled` | boolean | 否 | 是否请求 thinking |
| `thinking.effort` | string | 否 | provider 支持时覆盖 thinking effort |
| `thinking.budget` | positive integer | 否 | provider 支持时覆盖 thinking budget |
Expand All @@ -108,6 +109,8 @@ Authorization: Bearer <token>
| `alibabaCloud.securityToken` | string | 否 | 请求级 STS token |
| `alibabaCloud.regionId` | string | 否 | 请求级默认 region |

`llmHeaders` 遵循 `metadata.iac_code.llm_headers` 的 A2A context 绑定规则:传入后,同一 AG-UI thread 的后续请求即使省略该字段也会继承这些 headers;传入新的 map 会整体替换绑定,传入 `{}` 会清空绑定。header 值可能包含凭据,因此 adapter 不会持久化它们;本地 A2A 进程重启后,调用方需要重新传入。

`rosInvocationId` 的生命周期:

- initial run 与其 Interrupt Resume 必须使用相同值;
Expand Down
Loading