Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7d8ce64
Add transport-neutral human acting with standard ordered context
concordia-claw Sep 7, 2026
11deea3
Allow runtime acting policies in basic and minimal entity prefabs
concordia-claw Sep 7, 2026
d51615f
Add a reusable human-play example with terminal and mobile input
concordia-claw Sep 7, 2026
f8af843
Synchronize visual editor lifecycle status across tabs and reconnects
concordia-claw Sep 7, 2026
d47767b
Merge branch 'fix/editor-lifecycle-001', remote-tracking branch 'refs…
concordia-claw Sep 9, 2026
6a00673
Add shared attached operations and Bellwether human fixture
concordia-claw Sep 9, 2026
cd7c597
Allow bounded local Ollama generation requests
concordia-claw Sep 9, 2026
d93ba2a
Share optional context assembly across basic and minimal prefabs
concordia-claw Sep 9, 2026
f59e142
Add a complete three-watch Bellwether game with explicit consent and …
concordia-claw Sep 9, 2026
3116826
Add host-approved two-human Bellwether with mobile-safe role views
concordia-claw Sep 9, 2026
eee1915
Add opt-in local voice with editable transcripts and text fallback
concordia-claw Sep 9, 2026
ecbda20
Add tested Bellwether researcher recipes and explicit extension guide
concordia-claw Sep 9, 2026
4971460
Add standard Simultaneous council example with explicit batch accounting
concordia-claw Sep 9, 2026
b0d5d58
Add portable public Bellwether accounts without private traces
concordia-claw Sep 9, 2026
1b6b6d4
Explain human roles and preview Bellwether attempts without effects
concordia-claw Sep 9, 2026
d62cbad
Make Bellwether facility outcomes readable without color
concordia-claw Sep 9, 2026
fa413bf
Select trusted researcher recipes through the existing browser game
concordia-claw Sep 9, 2026
e29851a
Narrow standard actor types in recipe parity assertions
concordia-claw Sep 9, 2026
73cc6f5
Retain native focus and journal selection across Bellwether updates
concordia-claw Sep 9, 2026
d4df352
Export a public service figure with accessible provenance
concordia-claw Sep 9, 2026
25dd664
Support optional local embeddings through standard Bellwether memory
concordia-claw Sep 9, 2026
0f7c931
Add explicit local image-to-text drafts for human review
concordia-claw Sep 9, 2026
3762ee0
Review local visual notes before explicit action draft insertion
concordia-claw Sep 9, 2026
7f88839
Record public boundary explanations for Bellwether service outcomes
concordia-claw Sep 9, 2026
bfcd05c
Retain declared teaching setup in Bellwether public accounts
concordia-claw Sep 9, 2026
a623b68
Distinguish Coordinator Alice from the first watch in player labels
concordia-claw Sep 9, 2026
88bf6d2
Preserve scoped player drafts through late submission replies
concordia-claw Sep 9, 2026
0516e75
Support opt-in JSON schema text responses in Ollama
concordia-claw Sep 9, 2026
1a8bc5f
Honor standard Concat name-prefix option in minimal prefab
concordia-claw Sep 9, 2026
11c5e31
Separate Bellwether perception and structured resident actions
concordia-claw Sep 9, 2026
f483328
Inspect received developer snapshots lazily and preserve wire JSON
concordia-claw Sep 9, 2026
815cff9
Report unavailable resident responses without fabricated dialogue
concordia-claw Sep 9, 2026
d157d82
Distinguish interrupted Bellwether runs from completed accounts
concordia-claw Sep 9, 2026
1dc18a7
Coalesce attached SSE wakeups without eagerly building snapshots
concordia-claw Sep 9, 2026
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ __pycache__/

# Test cache
.pytest_cache

# Generated adventure logs and browser artifacts.
/runs/
79 changes: 79 additions & 0 deletions concordia/command_line_interface/concordia_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Copyright 2026 DeepMind Technologies Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Attach noninteractively to the same operation API used by the editor.

No engine or domain behavior lives here. JSON output is always machine-readable.
"""

import argparse
import json
import pathlib
import sys
import urllib.error
import urllib.request


def main(argv=None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--url', required=True, help='Trusted local service URL')
parser.add_argument('command', choices=('discover', 'state', 'call', 'watch'))
parser.add_argument(
'--input', default='-', help='JSON request file or - for stdin'
)
parser.add_argument('--timeout', type=float, default=15)
args = parser.parse_args(argv)
endpoint = {
'discover': 'operations',
'state': 'state',
'call': 'dispatch',
'watch': 'events',
}[args.command]
try:
data = None
if args.command == 'call':
raw = (
sys.stdin.read()
if args.input == '-'
else pathlib.Path(args.input).read_text(encoding='utf-8')
)
data = json.dumps(json.loads(raw), ensure_ascii=False).encode()
request = urllib.request.Request(
args.url.rstrip('/') + '/api/' + endpoint,
data=data,
headers={'Content-Type': 'application/json'},
)
with urllib.request.urlopen(request, timeout=args.timeout) as response:
if args.command == 'watch':
for line in response:
if line.startswith(b'data: '):
print(line[6:].decode().strip(), flush=True)
else:
print(response.read().decode())
return 0
except urllib.error.HTTPError as error:
print(error.read().decode(), file=sys.stderr)
return 2
except (OSError, ValueError) as error:
print(
json.dumps(
{'error': {'code': 'transport_or_input', 'message': str(error)}}
),
file=sys.stderr,
)
return 3


if __name__ == '__main__':
raise SystemExit(main())
1 change: 1 addition & 0 deletions concordia/components/agent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from concordia.components.agent import concat
from concordia.components.agent import concat_act_component
from concordia.components.agent import constant
from concordia.components.agent import human_act_component
from concordia.components.agent import instructions
from concordia.components.agent import memory
from concordia.components.agent import observation
Expand Down
32 changes: 32 additions & 0 deletions concordia/components/agent/concat.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,38 @@
from concordia.typing import entity_component


def validate_component_order(
component_order: Sequence[str] | None,
) -> tuple[str, ...] | None:
"""Snapshot an acting component's order, rejecting duplicate keys."""
order = tuple(component_order) if component_order is not None else None
if order is not None and len(set(order)) != len(order):
raise ValueError(
'The component order contains duplicate components: ' + ', '.join(order)
)
return order


def concat_contexts(
contexts: entity_component.ComponentContextMapping,
component_order: Sequence[str] | None = None,
) -> str:
"""Assemble pre-act contexts using ConcatAct's ordering and whitespace rules.

None uses mapping iteration order. An explicit order (including an empty
sequence) appends unspecified keys in sorted order. Empty values are omitted;
labels, whitespace and line breaks inside other values are kept verbatim.
Explicit keys absent from contexts raise KeyError. Constructors validate and
snapshot the order with validate_component_order.
"""
if component_order is None:
return '\n'.join(context for context in contexts.values() if context)
order = tuple(component_order) + tuple(
sorted(set(contexts.keys()) - set(component_order))
)
return '\n'.join(contexts[name] for name in order if contexts[name])


class Concatenate(
action_spec_ignored.ActionSpecIgnored, entity_component.ComponentWithLogging
):
Expand Down
30 changes: 8 additions & 22 deletions concordia/components/agent/concat_act_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
"""A simple acting component that aggregates contexts from components."""

from collections.abc import Sequence
from typing import cast
from typing import override

from concordia.components.agent import concat
from concordia.document import interactive_document
from concordia.language_model import language_model
from concordia.typing import entity as entity_lib
Expand Down Expand Up @@ -52,9 +54,8 @@ def __init__(
assembled in the iteration order of the `ComponentContextMapping` passed
to `get_action_attempt`. If the component order is specified, but does
not contain all the components passed to `get_action_attempt`, the
missing components will be appended at the end in the iteration order of
the `ComponentContextMapping` passed to `get_action_attempt`. The same
component cannot appear twice in the component order. All components in
missing components will be appended at the end in sorted key order.
The same component cannot appear twice in the component order. All components in
the component order must be in the `ComponentContextMapping` passed to
`get_action_attempt`.
prefix_entity_name: Whether to prefix the entity name to the output of
Expand All @@ -70,28 +71,13 @@ def __init__(
self._model = model
self._prefix_entity_name = prefix_entity_name
self._randomize_choices = randomize_choices
if component_order is None:
self._component_order = None
else:
self._component_order = tuple(component_order)
if self._component_order is not None:
if len(set(self._component_order)) != len(self._component_order):
raise ValueError(
'The component order contains duplicate components: '
+ ', '.join(self._component_order)
)
self._component_order = concat.validate_component_order(component_order)

def _context_for_action(
self,
contexts: entity_component.ComponentContextMapping,
) -> str:
if self._component_order is None:
return '\n'.join(context for context in contexts.values() if context)
else:
order = self._component_order + tuple(
sorted(set(contexts.keys()) - set(self._component_order))
)
return '\n'.join(contexts[name] for name in order if contexts[name])
return concat.concat_contexts(contexts, self._component_order)

@override
def get_action_attempt(
Expand Down Expand Up @@ -179,8 +165,8 @@ def get_state(self) -> entity_component.ComponentState:

def set_state(self, state: entity_component.ComponentState) -> None:
if 'component_order' in state:
order = state['component_order']
self._component_order = tuple(order) if order else None # pyrefly: ignore[bad-argument-type]
order = cast(Sequence[str] | None, state['component_order'])
self._component_order = tuple(order) if order else None
if 'prefix_entity_name' in state:
self._prefix_entity_name = state['prefix_entity_name'] # pyrefly: ignore[bad-assignment]
if 'randomize_choices' in state:
Expand Down
136 changes: 136 additions & 0 deletions concordia/components/agent/context_order_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Copyright 2026 DeepMind Technologies Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Human and LLM acting share Concat's exact established context semantics."""

from unittest import mock

from absl.testing import absltest
from absl.testing import parameterized
from concordia.agents import entity_agent_with_logging
from concordia.components.agent import concat_act_component
from concordia.components.agent import human_act_component
from concordia.language_model import no_language_model
from concordia.typing import entity as entity_lib


class ContextOrderTest(parameterized.TestCase):

def policies(self, order):
reader = mock.Mock(return_value='LOOK')
model = mock.Mock(wraps=no_language_model.NoLanguageModel())
model.sample_text.return_value = 'LOOK'
human = human_act_component.HumanActComponent(reader, component_order=order)
llm = concat_act_component.ConcatActComponent(model, component_order=order)
for policy in (human, llm):
entity_agent_with_logging.EntityAgentWithLogging('Ilyra', policy)
return human, llm, reader, model

@parameterized.named_parameters(
('mapping_order', None, {'z': 'Z', 'a': 'A'}, 'Z\nA'),
('empty_order_sorts', [], {'z': 'Z', 'a': 'A'}, 'A\nZ'),
(
'partial_order_sorts_remainder',
['b'],
{'z': 'Z', 'b': 'B', 'a': 'A'},
'B\nA\nZ',
),
('explicit_order', ['b', 'a'], {'a': 'A', 'b': 'B'}, 'B\nA'),
(
'preserve_labels_and_whitespace',
['z', 'empty'],
{'empty': '', 'z': '\nLabel:\n Z\n', 'a': ' A '},
'\nLabel:\n Z\n\n A ',
),
('empty_context', None, {}, ''),
)
def test_exact_context_matches_concat_prompt(self, order, contexts, expected):
human, llm, reader, model = self.policies(order)
spec = entity_lib.free_action_spec(call_to_action='Your move, {name}?')
human.get_action_attempt(contexts, spec)
llm.get_action_attempt(contexts, spec)
request = reader.call_args.args[0]
self.assertEqual(request.context, expected)
self.assertEqual(llm._context_for_action(contexts), expected)
self.assertEqual(
human.get_context_concat_order(), llm.get_context_concat_order()
)
prompt = model.sample_text.call_args.kwargs['prompt']
self.assertTrue(prompt.startswith(expected + '\n'), repr(prompt))
self.assertEqual(
human.get_entity().get_last_log()['__act__']['Context'], expected
)

@parameterized.parameters(
human_act_component.HumanActComponent,
concat_act_component.ConcatActComponent,
)
def test_duplicate_order_is_rejected(self, policy):
with self.assertRaisesRegex(ValueError, 'duplicate components: x, x'):
policy(mock.Mock(), component_order=['x', 'x'])

def test_explicit_missing_key_raises_without_asking_human_or_llm(self):
human, llm, reader, model = self.policies(['missing'])
for policy in (human, llm):
with self.assertRaises(KeyError):
policy.get_action_attempt(
{'present': 'value'}, entity_lib.DEFAULT_ACTION_SPEC
)
reader.assert_not_called()
model.sample_text.assert_not_called()

def test_order_and_request_context_are_snapshots_and_retries_keep_string(
self,
):
order = ['b', 'a']
human, llm, reader, _ = self.policies(order)
order.reverse()
self.assertEqual(human.get_context_concat_order(), ('b', 'a'))
self.assertEqual(llm.get_context_concat_order(), ('b', 'a'))
contexts = {'a': 'A', 'b': 'B'}
requests = []

def read(request):
requests.append(request)
contexts['b'] = 'changed'
return '' if len(requests) == 1 else 'LOOK'

reader.side_effect = read
human.get_action_attempt(contexts, entity_lib.DEFAULT_ACTION_SPEC)
self.assertEqual([r.context for r in requests], ['B\nA', 'B\nA'])
self.assertEqual(requests[1].contexts['b'], 'B')

@parameterized.parameters((None,), ([],), (['b', 'a'],))
def test_order_state_format_and_partial_restore_match_concat(self, order):
human, llm, _, _ = self.policies(order)
self.assertEqual(
human.get_state()['component_order'], llm.get_state()['component_order']
)
# Retain Concat's legacy empty-sequence serialization/restore semantics.
for state in (
human.get_state(),
{},
{'component_order': []},
{'component_order': ['a']},
):
human.set_state(state)
llm.set_state(state)
self.assertEqual(
human.get_context_concat_order(), llm.get_context_concat_order()
)
self.assertNotIn('input_reader', human.get_state())


if __name__ == '__main__':
absltest.main()
Loading