Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/*
* Copyright 2024-2026 the original author or authors.
*
* 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
*
* http://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.
*/
package io.agentscope.examples.documentation2.harness.teams;

import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.core.message.Msg;
import io.agentscope.core.message.TextBlock;
import io.agentscope.core.model.ChatResponse;
import io.agentscope.core.model.GenerateOptions;
import io.agentscope.core.model.Model;
import io.agentscope.core.model.ToolSchema;
import io.agentscope.core.session.SessionLogStore;
import io.agentscope.core.session.SessionProjection;
import io.agentscope.harness.agent.HarnessAgent;
import io.agentscope.harness.agent.filesystem.local.LocalFilesystem;
import io.agentscope.harness.agent.filesystem.remote.store.InMemoryStore;
import io.agentscope.harness.agent.session.WorkspaceSessionLogStore;
import io.agentscope.harness.agent.team.LocalTeamClient;
import io.agentscope.harness.agent.team.TeamCreateSpec;
import io.agentscope.harness.agent.team.TeamMemberSpec;
import io.agentscope.harness.agent.team.TeamSessionMembership.MemberSession;
import io.agentscope.harness.agent.team.TeamSessionMembership.Role;
import io.agentscope.harness.agent.team.TeamSessionMembership.SessionKey;
import io.agentscope.harness.agent.team.TeamSessionMembership.TeamAddress;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import reactor.core.publisher.Flux;

/** Registers existing native Sessions through the programming API, without provider credentials. */
public final class NativeTeamMembershipExample {
private static final Duration WAIT = Duration.ofSeconds(20);
private static final String LOG_DOMAIN = "example-workspace-logs";
private static final String RELATIONSHIP_DOMAIN = "example-relations";

private NativeTeamMembershipExample() {}

/** Runs with an optional workspace directory. Only the Session logs survive process exit. */
public static void main(String[] args) throws Exception {
Path workspace =
args.length == 0
? Files.createTempDirectory("native-team-membership-")
: Path.of(args[0]);
var logs = new WorkspaceSessionLogStore(new LocalFilesystem(workspace));
var leaderContext = context();
var workerContext = context();
MemberSession leader;
MemberSession worker;
try (var leaderAgent = agent(workspace, logs, "coordinator-runtime");
var workerAgent = agent(workspace, logs, "research-runtime")) {
leaderAgent.call("Initialize the coordinator conversation.", leaderContext).block(WAIT);
workerAgent.call("Initialize the researcher conversation.", workerContext).block(WAIT);
leader =
new MemberSession(
"lead",
"demo",
"coordinator-definition",
Role.LEADER,
SessionKey.nativeSession(
LOG_DOMAIN, leaderAgent.sessionKey(leaderContext)));
worker =
new MemberSession(
"researcher",
"demo",
"research-definition",
Role.WORKER,
SessionKey.nativeSession(
LOG_DOMAIN, workerAgent.sessionKey(workerContext)));
}

var teamStore = new InMemoryStore();
var client = new LocalTeamClient(teamStore);
var address = new TeamAddress("example-app", "research");
client.createTeam(
new TeamCreateSpec(
address.teamName(),
address.namespace(),
"Compare evidence",
"coordinator-definition",
"",
List.of(
new TeamMemberSpec(
"researcher", "research-definition", "", "byo"))))
.block(WAIT);
var membership =
client.sessionMembership(RELATIONSHIP_DOMAIN, Map.of(), Map.of(LOG_DOMAIN, logs));
var team =
Objects.requireNonNull(
membership
.adoptTeam("demo", address, "Research", List.of(leader))
.block(WAIT));
var binding =
Objects.requireNonNull(membership.bindMember("demo", address, worker).block(WAIT));

var reopenedLogs = new WorkspaceSessionLogStore(new LocalFilesystem(workspace));
var reopened =
new LocalTeamClient(teamStore)
.sessionMembership(
RELATIONSHIP_DOMAIN, Map.of(), Map.of(LOG_DOMAIN, reopenedLogs));
var current = reopened.findMembership(worker.session()).block(WAIT);
var reopenedTeam = Objects.requireNonNull(reopened.getTeam("demo", address).block(WAIT));
var members = Objects.requireNonNull(reopened.listMemberships("demo", address).block(WAIT));
if (!binding.equals(current)
|| !team.teamId().equals(reopenedTeam.teamId())
|| members.size() != 2) {
throw new IllegalStateException("Reopening changed the Team associations");
}

boolean removed =
Boolean.TRUE.equals(
reopened.unbindMember("demo", address, worker.session(), binding.token())
.block(WAIT));
if (!removed || reopened.findMembership(worker.session()).block(WAIT) != null) {
throw new IllegalStateException("The expected binding was not removed");
}
var nativeKey =
new io.agentscope.core.session.SessionKey(
worker.session().owner(),
worker.session().agentId(),
worker.session().sessionId());
if (SessionProjection.read(reopenedLogs.open(nativeKey, workerContext)).stateJson()
== null) {
throw new IllegalStateException("Removing membership deleted the native Session");
}
System.out.println("Team: " + team.teamId());
System.out.println("Registered two existing Sessions and recovered both associations.");
System.out.println(
"Removed the researcher association; its committed Session log remains.");
System.out.println("Native log workspace: " + workspace.toAbsolutePath());
System.out.println(
"Team records use InMemoryStore in this example. Use a durable JdbcStore to retain"
+ " associations across process restarts.");
}

private static RuntimeContext context() {
return RuntimeContext.builder()
.userId("demo")
.sessionId(UUID.randomUUID().toString())
.build();
}

private static HarnessAgent agent(Path workspace, SessionLogStore logs, String agentId) {
return HarnessAgent.builder()
.agentId(agentId)
.workspace(workspace)
.model(new InitializationModel())
.sessionLogStore(logs)
.disableShellTool()
.disableFilesystemTools()
.disableWebTools()
.disableSubagents()
.disableDynamicSubagents()
.disableDynamicSkills()
.disableDefaultWorkspaceSkills()
.disableMemoryTools()
.disableMemoryHooks()
.disableToolsConfig()
.disableWorkspaceContext()
.disableAtPathExpansion()
.build();
}

private static final class InitializationModel implements Model {
@Override
public String getModelName() {
return "scripted-session-initialization";
}

@Override
public Flux<ChatResponse> stream(
List<Msg> messages, List<ToolSchema> tools, GenerateOptions options) {
return Flux.just(
ChatResponse.builder()
.content(List.of(TextBlock.builder().text("Session ready.").build()))
.build());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright 2024-2026 the original author or authors.
*
* 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
*
* http://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.
*/
package io.agentscope.extensions.jdbc;

import java.sql.SQLException;
import java.util.Set;

/** Shared duplicate-key classification for JDBC stores and repositories. */
public final class JdbcConstraintErrors {

// SQLState 23000 covers all integrity constraints; the vendor code identifies duplicates.
// MySQL/MariaDB 1062 and 1022, Oracle ORA-00001, SQL Server 2601 and 2627.
private static final Set<Integer> DUPLICATE_KEY_VENDOR_CODES =
Set.of(1062, 1022, 1, 2601, 2627);
// Informix/GBase 8s may report these duplicate codes without a SQLState.
private static final Set<Integer> VENDOR_UNIQUE_VIOLATION_CODES = Set.of(-239, -268);

private JdbcConstraintErrors() {}

/**
* Recognizes specific unique/primary-key violations, never the general integrity category.
*
* <p>SQLState 23505 identifies unique violations in PostgreSQL, H2 and compatible drivers.
* SQLite's error 19 covers all constraints, so its optional driver is queried reflectively
* for the precise result code. Unknown diagnostics or unavailable result codes return false;
* callers must propagate those storage failures rather than treating them as contention.
*
* @param e the JDBC failure to classify
* @return true only for a positively identified duplicate-key conflict
*/
public static boolean isDuplicateKey(SQLException e) {
if ("23505".equals(e.getSQLState())) {
return true;
}
if ("23000".equals(e.getSQLState())
&& DUPLICATE_KEY_VENDOR_CODES.contains(e.getErrorCode())) {
return true;
}
if (e.getErrorCode() == 19 && e.getClass().getName().startsWith("org.sqlite.")) {
try {
Object result = e.getClass().getMethod("getResultCode").invoke(e);
if (result instanceof Enum<?> code) {
return "SQLITE_CONSTRAINT_PRIMARYKEY".equals(code.name())
|| "SQLITE_CONSTRAINT_UNIQUE".equals(code.name());
}
} catch (ReflectiveOperationException ignored) {
// An unknown constraint is not evidence of a duplicate key.
}
}
return VENDOR_UNIQUE_VIOLATION_CODES.contains(e.getErrorCode());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.agentscope.core.skill.repository.AgentSkillRepository;
import io.agentscope.core.skill.repository.AgentSkillRepositoryInfo;
import io.agentscope.core.util.JsonUtils;
import io.agentscope.extensions.jdbc.JdbcConstraintErrors;
import io.agentscope.extensions.jdbc.dialect.AbstractJdbcDialect;
import io.agentscope.extensions.jdbc.dialect.BoundSql;
import io.agentscope.extensions.jdbc.dialect.table.SkillDialect;
Expand All @@ -28,14 +29,12 @@
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLIntegrityConstraintViolationException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import javax.sql.DataSource;
import org.slf4j.Logger;
Expand Down Expand Up @@ -100,38 +99,6 @@ public class JdbcAgentSkillRepository implements AgentSkillRepository {
*/
private static final Pattern DRIVE_LETTER_PREFIX = Pattern.compile("^[A-Za-z]:.*");

/** SQLite's {@code SQLITE_CONSTRAINT}; xerial reports it with a null SQLState. */
private static final int SQLITE_CONSTRAINT = 19;

/**
* Duplicate-key codes for drivers reporting no SQLState at all and not covered by {@link
* #SQLITE_CONSTRAINT}: Informix and GBase 8s — {@code -239} (duplicate value in a unique
* index) and {@code -268} (unique constraint violation). Values from the vendor
* documentation; those drivers were not available to verify against, unlike every other
* entry in {@link #isUniqueViolation}.
*/
private static final Set<Integer> VENDOR_UNIQUE_VIOLATION_CODES = Set.of(-239, -268);

/**
* SQLStates that positively report a unique-constraint violation: {@code 23505} (the
* SQL:2003 code — PostgreSQL and its forks, H2, HSQLDB, Derby, DB2). The umbrella {@code
* 23500} is deliberately absent — no driver reporting it for duplicates could be named,
* and umbrella states cover sibling violations; those stacks' duplicates reach {@link
* #DUPLICATE_KEY_VENDOR_CODES} under {@code 23000} instead. Should such a driver ever
* surface, the remedy is widening that gate to its class-23 state, not reinstating the
* umbrella.
*/
private static final Set<String> UNIQUE_SQL_STATES = Set.of("23505");

/**
* Duplicate-key vendor codes under the generic integrity SQLState {@code 23000}, where
* the state alone cannot tell a duplicate from sibling violations: MySQL / MariaDB /
* TiDB / OceanBase {@code 1062}, MySQL 5.5-era {@code 1022}, Oracle {@code ORA-00001}
* = 1, SQL Server {@code 2601} / {@code 2627}.
*/
private static final Set<Integer> DUPLICATE_KEY_VENDOR_CODES =
Set.of(1062, 1022, 1, 2601, 2627);

/** The data source holding the skill tables; never closed by this repository. */
private final DataSource dataSource;

Expand Down Expand Up @@ -1027,68 +994,11 @@ private static void bindParams(PreparedStatement ps, List<Object> params) throws
* duplicate signals match, so any other constraint failure surfaces as the driver's own
* {@link SQLException} instead of the destructive "use force=true" advice.
*
* <p>Signals, in order:
*
* <ol>
* <li>SQLState in {@link #UNIQUE_SQL_STATES} — the portable unique-violation codes.
* pgjdbc and its forks never throw the JDBC 4 subtypes (issue #963 is still open),
* nor do Firebird, SQL Server, 达梦, or H2 1.x; all report 23505, as do H2 2.x,
* HSQLDB, Derby, and DB2 through the subtype. pgjdbc returns error code 0, so the
* code cannot stand in for the state.
* <li>SQLState {@code 23000} plus a code from {@link #DUPLICATE_KEY_VENDOR_CODES} —
* that state is the whole class-23 umbrella for the MySQL family, Oracle, and SQL
* Server, so the vendor code decides.
* <li>SQLite (xerial): error 19 with a null state is every {@code
* SQLITE_CONSTRAINT_*} collapsed; {@link #isSqliteUniqueViolation} narrows it.
* <li>Vendor codes for drivers reporting no SQLState: {@link
* #VENDOR_UNIQUE_VIOLATION_CODES}.
* </ol>
*
* <p>Deliberately not matched: the {@link SQLIntegrityConstraintViolationException}
* subtype on its own — MySQL Connector/J also raises it for NOT NULL (1048) and foreign
* key (1451/1452) violations, which is exactly the misreport this method must avoid —
* and the class-23 siblings (23502, 23503, 23514). An unmatched duplicate fails the
* save as a plain {@code SQLException} rather than a false conflict.
*
* <p>Also not matched: MySQL {@code ER_DUP_ENTRY_AUTOINCREMENT_CASE} (1569) reports
* SQLState {@code HY000} — outside class 23 — but it requires an explicit auto-increment
* value, which the skill insert never supplies. ClickHouse has no duplicate-key error to
* match: it does not enforce primary-key uniqueness.
*
* @param e the exception thrown by the skill insert
* @return true when the failure is a duplicate-key conflict rather than any other error
*/
static boolean isUniqueViolation(SQLException e) {
String state = e.getSQLState();
// Null SQLState is real (SQLite reports one) and Set.of rejects null lookups.
if (state != null && UNIQUE_SQL_STATES.contains(state)) {
return true;
}
if ("23000".equals(state) && DUPLICATE_KEY_VENDOR_CODES.contains(e.getErrorCode())) {
return true;
}
if (e.getErrorCode() == SQLITE_CONSTRAINT
&& e.getClass().getName().startsWith("org.sqlite.")) {
return isSqliteUniqueViolation(e);
}
return VENDOR_UNIQUE_VIOLATION_CODES.contains(e.getErrorCode());
}

/**
* Narrows SQLite's umbrella error 19 to unique violations via the driver's {@code
* getResultCode()}, reached reflectively to keep the driver off the compile classpath.
* Compares the enum's {@code name()}, not {@code toString()} — the latter renders the
* full message and never equals the bare constant (sqlite-jdbc 3.47.1.0). Falls back to
* the umbrella signal if reflection fails.
*/
private static boolean isSqliteUniqueViolation(SQLException e) {
try {
Object resultCode = e.getClass().getMethod("getResultCode").invoke(e);
return resultCode instanceof Enum<?>
&& "SQLITE_CONSTRAINT_UNIQUE".equals(((Enum<?>) resultCode).name());
} catch (ReflectiveOperationException reflectionFailure) {
return true;
}
return JdbcConstraintErrors.isDuplicateKey(e);
}

/**
Expand Down
Loading
Loading